Leon4gr45 commited on
Commit
bafff95
·
verified ·
1 Parent(s): beaee29

Upload folder using huggingface_hub (part 10)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. tools/unknown.py.dox.md +3 -48
  2. tools/vision_load.py +3 -233
  3. tools/vision_load.py.dox.md +3 -62
  4. tools/wait.py +3 -89
  5. tools/wait.py.dox.md +3 -54
  6. update_reqs.py +3 -38
  7. usr/agents/.gitkeep +0 -0
  8. usr/knowledge/.gitkeep +0 -0
  9. usr/knowledge/main/.gitkeep +0 -0
  10. usr/knowledge/solutions/.gitkeep +0 -0
  11. usr/plugins/.gitkeep +0 -0
  12. usr/skills/.gitkeep +3 -0
  13. usr/workdir/.gitkeep +0 -0
  14. webui/AGENTS.md +3 -60
  15. webui/components/AGENTS.md +3 -77
  16. webui/components/_examples/AGENTS.md +3 -27
  17. webui/components/_examples/_example-component.html +3 -36
  18. webui/components/_examples/_example-store.js +3 -23
  19. webui/components/canvas/AGENTS.md +3 -33
  20. webui/components/canvas/right-canvas-store.js +3 -620
  21. webui/components/canvas/right-canvas.css +3 -384
  22. webui/components/canvas/right-canvas.html +3 -132
  23. webui/components/chat/AGENTS.md +3 -47
  24. webui/components/chat/attachments/attachmentsStore.js +3 -511
  25. webui/components/chat/attachments/dragDropOverlay.html +3 -28
  26. webui/components/chat/attachments/inputPreview.html +3 -30
  27. webui/components/chat/input/bottom-actions-bar.html +3 -98
  28. webui/components/chat/input/bottom-actions.html +3 -161
  29. webui/components/chat/input/chat-bar-input.html +3 -457
  30. webui/components/chat/input/chat-bar.html +3 -52
  31. webui/components/chat/input/composer-banner-store.js +3 -69
  32. webui/components/chat/input/input-store.js +3 -780
  33. webui/components/chat/input/progress.html +3 -57
  34. webui/components/chat/message-queue/message-queue-store.js +3 -245
  35. webui/components/chat/message-queue/message-queue.html +3 -245
  36. webui/components/chat/model-gate-store.js +3 -233
  37. webui/components/chat/model-setup-gate.html +3 -203
  38. webui/components/chat/navigation/chat-navigation-store.js +3 -127
  39. webui/components/chat/top-section/chat-top-store.js +3 -13
  40. webui/components/chat/top-section/chat-top.html +3 -42
  41. webui/components/dropdown/AGENTS.md +3 -26
  42. webui/components/dropdown/dropdown.html +3 -69
  43. webui/components/messages/AGENTS.md +3 -38
  44. webui/components/messages/action-buttons/simple-action-buttons.css +3 -153
  45. webui/components/messages/action-buttons/simple-action-buttons.js +3 -134
  46. webui/components/messages/process-group/process-group-dom.js +3 -76
  47. webui/components/messages/process-group/process-group.css +3 -914
  48. webui/components/messages/resize/message-resize-store.js +3 -137
  49. webui/components/modals/AGENTS.md +3 -34
  50. webui/components/modals/context/context-store.js +3 -132
tools/unknown.py.dox.md CHANGED
@@ -1,48 +1,3 @@
1
- # unknown.py DOX
2
-
3
- ## Purpose
4
-
5
- - Own the `unknown.py` agent tool.
6
- - This module handles unknown or malformed tool-call requests.
7
- - Keep this file-level DOX profile synchronized with `unknown.py` because this directory is intentionally flat.
8
-
9
- ## Ownership
10
-
11
- - `unknown.py` owns the runtime implementation.
12
- - `unknown.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
13
- - Classes:
14
- - `Unknown` (`Tool`)
15
- - `async execute(self, **kwargs)`
16
-
17
- ## Runtime Contracts
18
-
19
- - Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
20
- - Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
21
- - `Unknown` is a `Tool`.
22
- - `Unknown` defines `execute(...)`.
23
- - Imported dependency areas include: `extensions.python.system_prompt._11_tools_prompt`, `helpers.tool`.
24
-
25
- ## Key Concepts
26
-
27
- - Important called helpers/classes observed in the source: `Response`, `build_tools_prompt`, `self.agent.read_prompt`.
28
- - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
29
-
30
- ## Work Guidance
31
-
32
- - Keep tool output concise, model-readable, and safe for history persistence.
33
- - Coordinate argument or behavior changes with prompt tool instructions and skill guidance.
34
- - Respect intervention flow for long-running, external, or user-visible operations.
35
-
36
- ## Verification
37
-
38
- - Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
39
- - Related tests observed by source search:
40
- - `tests/test_file_tree_visualize.py`
41
- - `tests/test_oauth_providers.py`
42
- - `tests/test_socketio_unknown_namespace.py`
43
- - `tests/test_tunnel_remote_link.py`
44
- - `tests/test_ws_manager.py`
45
-
46
- ## Child DOX Index
47
-
48
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33edae4ca6280348d9de290a5f09cf28a9f5cfb2b3b843853693b9dca3404388
3
+ size 1887
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/vision_load.py CHANGED
@@ -1,233 +1,3 @@
1
- from mimetypes import guess_type
2
-
3
- from langchain_core.messages import HumanMessage
4
-
5
- from helpers import (
6
- chat_media,
7
- ephemeral_images,
8
- files,
9
- history,
10
- images,
11
- parallel_tools,
12
- runtime,
13
- )
14
- from helpers.tool import Response, Tool
15
- from plugins._model_config.helpers.model_config import (
16
- build_vision_model,
17
- get_chat_model_config,
18
- get_vision_model_config,
19
- )
20
-
21
- # image token estimation for context window
22
- TOKENS_ESTIMATE = 1500
23
-
24
-
25
- class VisionLoad(Tool):
26
- async def execute(
27
- self,
28
- paths: list[str] | str = [],
29
- query: str = "",
30
- **kwargs,
31
- ) -> Response:
32
-
33
- self.images_dict = {}
34
- self.loaded_paths: list[str] = []
35
- self.skipped_paths: list[str] = []
36
- self.vision_config = get_vision_model_config(self.agent)
37
- if isinstance(paths, str):
38
- paths = [paths]
39
- if not isinstance(paths, list):
40
- return Response(
41
- message="vision_load error: `paths` must be a string or an array.",
42
- break_loop=False,
43
- )
44
-
45
- max_embeds = self._get_max_embeds()
46
- requested = [
47
- (str(path or "").strip(), self._display_input_path(str(path or "").strip(), idx + 1))
48
- for idx, path in enumerate(paths)
49
- ]
50
- limited_paths = requested if max_embeds <= 0 else requested[-max_embeds:]
51
- self.skipped_paths = (
52
- [display for _, display in requested[:-max_embeds]]
53
- if max_embeds > 0 and len(requested) > max_embeds
54
- else []
55
- )
56
-
57
- for idx, (path, display_path) in enumerate(limited_paths):
58
- if not path:
59
- continue
60
- if ephemeral_images.is_ref(path):
61
- image = ephemeral_images.consume_image(
62
- path,
63
- context_id=self._context_id(),
64
- )
65
- if image is None:
66
- continue
67
- display = image.display_name or display_path
68
- stored_ref = self._store_ephemeral_image(image)
69
- if stored_ref:
70
- self.images_dict[display] = stored_ref
71
- self.loaded_paths.append(display)
72
- continue
73
- if self._is_data_image_url(path):
74
- stored_ref = self._store_data_url(path, preferred_name=f"vision-load-{idx + 1}.png")
75
- if stored_ref:
76
- self.images_dict[display_path] = stored_ref
77
- self.loaded_paths.append(display_path)
78
- continue
79
- if not await runtime.call_development_function(files.exists, str(path)):
80
- continue
81
-
82
- if path not in self.images_dict:
83
- mime_type, _ = guess_type(str(path))
84
- if mime_type and mime_type.startswith("image/"):
85
- try:
86
- stored_ref = self._store_local_image(path, preferred_name=files.basename(path))
87
- self.images_dict[display_path] = stored_ref
88
- self.loaded_paths.append(display_path)
89
- except (FileNotFoundError, OSError, ValueError):
90
- continue
91
-
92
- message = self._summary() if self.images_dict or self.skipped_paths else "No images processed"
93
- if self.vision_config and self.images_dict:
94
- try:
95
- capsule = await self._call_vision_model(
96
- list(self.images_dict.values()),
97
- query,
98
- )
99
- message = (
100
- f"Analyzed {len(self.images_dict)} image(s)"
101
- f"; {len(self.skipped_paths)} skipped.\n\n{capsule.strip()}"
102
- )
103
- except Exception as exc:
104
- message = f"Image analysis error: {str(exc)[:1000]}"
105
- return Response(message=message, break_loop=False)
106
-
107
- def _get_max_embeds(self) -> int:
108
- cfg = self.vision_config or get_chat_model_config(self.agent)
109
- return int(cfg.get("max_embeds", 10) or 0)
110
-
111
- def _context_id(self) -> str:
112
- context = getattr(self.agent, "context", None)
113
- get_data = getattr(context, "get_data", None)
114
- parent_id = (
115
- get_data(parallel_tools.PARALLEL_WORKER_PARENT_CONTEXT_KEY)
116
- if get_data
117
- else ""
118
- )
119
- return str(parent_id or getattr(context, "id", "") or "").strip()
120
-
121
- async def _call_vision_model(self, image_paths: list[str], query: str) -> str:
122
- user_message = getattr(self.agent, "last_user_message", None)
123
- output_text = getattr(user_message, "output_text", None)
124
- request = str(output_text() if callable(output_text) else "").strip()
125
- content = [
126
- {
127
- "type": "text",
128
- "text": self.agent.read_prompt(
129
- "fw.vision_load.md",
130
- request=request,
131
- query=str(query or "").strip(),
132
- ),
133
- }
134
- ]
135
- content.extend(
136
- {"type": "image_url", "image_url": {"url": path}}
137
- for path in image_paths
138
- )
139
- response, _ = await build_vision_model(self.agent).unified_call(
140
- messages=[HumanMessage(content=content)],
141
- )
142
- if not str(response or "").strip():
143
- raise RuntimeError("Vision Model returned an empty response.")
144
- return str(response)
145
-
146
- def _store_ephemeral_image(self, image: ephemeral_images.EphemeralImage) -> str:
147
- context_id = self._context_id()
148
- if not context_id:
149
- return image.data_url
150
- source = chat_media.infer_source(image.ref, image.display_name)
151
- category = chat_media.category_for_source(source)
152
- saved = chat_media.save_image_base64(
153
- context_id=context_id,
154
- data=image.data,
155
- mime_type=image.mime,
156
- category=category,
157
- source=source,
158
- preferred_name=image.display_name,
159
- )
160
- return saved.a0_path
161
-
162
- def _store_data_url(self, data_url: str, *, preferred_name: str = "") -> str:
163
- context_id = self._context_id()
164
- if not context_id:
165
- return data_url
166
- source = chat_media.infer_source(data_url, preferred_name)
167
- category = chat_media.category_for_source(source)
168
- saved = chat_media.save_image_data_url(
169
- context_id=context_id,
170
- data_url=data_url,
171
- category=category,
172
- source=source,
173
- preferred_name=preferred_name,
174
- )
175
- return saved.a0_path
176
-
177
- def _store_local_image(self, path: str, *, preferred_name: str = "") -> str:
178
- context_id = self._context_id()
179
- if not context_id:
180
- return images.to_data_url(path)
181
- return chat_media.materialize_image_ref(
182
- context_id=context_id,
183
- url=path,
184
- source=chat_media.infer_source(path, preferred_name),
185
- preferred_name=preferred_name,
186
- )
187
-
188
- def _summary(self) -> str:
189
- loaded = "\n".join(self.loaded_paths) if self.loaded_paths else "none"
190
- summary = f"Loaded images ({len(self.loaded_paths)}):\n{loaded}"
191
- if self.skipped_paths:
192
- summary += (
193
- f"\n\nSkipped images ({len(self.skipped_paths)}, max {self._get_max_embeds()}):\n"
194
- + "\n".join(self.skipped_paths)
195
- )
196
- return summary
197
-
198
- @staticmethod
199
- def _is_data_image_url(value: str) -> bool:
200
- normalized = str(value or "").strip().lower()
201
- return normalized.startswith("data:image/") and ";base64," in normalized
202
-
203
- @classmethod
204
- def _display_input_path(cls, value: str, index: int) -> str:
205
- if ephemeral_images.is_ref(value):
206
- return ephemeral_images.display_ref(value)
207
- if cls._is_data_image_url(value):
208
- prefix = value.split(",", 1)[0]
209
- return f"{prefix},<ephemeral-image-{index}>"
210
- return value
211
-
212
- async def after_execution(self, response: Response, **kwargs):
213
- await super().after_execution(response, **kwargs)
214
- if self.images_dict and not self.vision_config:
215
- content = [
216
- {"type": "image_url", "image_url": {"url": image_path}}
217
- for image_path in self.images_dict.values()
218
- ]
219
- raw_message = history.RawMessage(
220
- raw_content=content,
221
- preview="<Image attachments loaded by path>",
222
- )
223
- tokens = TOKENS_ESTIMATE * len(content)
224
- if not parallel_tools.queue_parallel_parent_history(
225
- self.agent,
226
- content=raw_message,
227
- tokens=tokens,
228
- ):
229
- self.agent.hist_add_message(
230
- False,
231
- content=raw_message,
232
- tokens=tokens,
233
- )
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c32e18ed5acc62ee2a24ec59a24c8ad8f55013d4f47b2aaacf2d968e1b835267
3
+ size 8861
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/vision_load.py.dox.md CHANGED
@@ -1,62 +1,3 @@
1
- # vision_load.py DOX
2
-
3
- ## Purpose
4
-
5
- - Own the `vision_load.py` agent tool.
6
- - This module routes images either into Main model-visible content or through the preset's optional Vision Model.
7
- - Keep this file-level DOX profile synchronized with `vision_load.py` because this directory is intentionally flat.
8
-
9
- ## Ownership
10
-
11
- - `vision_load.py` owns the runtime implementation.
12
- - `vision_load.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
13
- - Classes:
14
- - `VisionLoad` (`Tool`)
15
- - `async execute(self, paths, query="", **kwargs) -> Response`
16
- - `async after_execution(self, response: Response, **kwargs)`
17
- - Notable constants/configuration names: `TOKENS_ESTIMATE`.
18
-
19
- ## Runtime Contracts
20
-
21
- - Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
22
- - One call may contain multiple paths; a bare string is treated as one path. The Vision Model route sends every selected path in one request and returns one textual capsule.
23
- - Model configuration exposes a Vision Model only when the effective preset selects that route; otherwise this tool follows Main's native vision path.
24
- - The public tool contract is route-agnostic and accepts an optional focused `query`. A Vision Model receives both that query and the current user request through `fw.vision_load.md`; direct parallel workers inherit the request from their parent.
25
- - Native Main vision already retains the query in its authored tool-call transcript, so native raw history remains image-only and does not repeat model-authored instructions as user content.
26
- - Delegation completes during `execute(...)` so native Responses function output contains the real capsule before `after_execution(...)` persists it.
27
- - Delegated history contains the text capsule only. Native history contains the tool result followed by one raw message holding all loaded image blocks.
28
- - In a direct parallel worker, native image content is queued for the parent and promoted immediately after the outer `parallel` result; the disposable worker never owns the only copy of model-visible pixels.
29
- - Direct parallel workers inherit the parent's model override generically. This tool uses their recorded parent context only to resolve ephemeral refs and durable chat media.
30
- - `max_embeds` comes from the model that actually receives the images.
31
- - Native tool-result summaries omit the skipped-images section when no images were skipped; when the limit drops inputs, the section reports the skipped paths and active maximum.
32
- - Vision Model calls use the selected model's Advanced `kwargs`; this tool does not impose a separate timeout or output-token limit.
33
- - An empty Vision Model response is reported as an image-analysis error instead of a successful empty capsule.
34
- - Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
35
- - `VisionLoad` is a `Tool`.
36
- - `VisionLoad` defines `execute(...)`.
37
- - Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence, secret handling.
38
- - Imported dependency areas include: `helpers`, `helpers.tool`, `langchain_core.messages`, `mimetypes`, and `_model_config`.
39
-
40
- ## Key Concepts
41
-
42
- - Important called helpers/classes observed in the source: `build_vision_model`, `get_vision_model_config`, `self._get_max_embeds`, `Response`, `self._context_id`, `chat_media.save_image_base64`, `chat_media.save_image_data_url`, `chat_media.materialize_image_ref`, `ephemeral_images.consume_image`, `images.to_data_url`, `history.RawMessage`, `super().after_execution`, `model.unified_call`.
43
- - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
44
-
45
- ## Work Guidance
46
-
47
- - Keep tool output concise, model-readable, and safe for history persistence.
48
- - Coordinate argument or behavior changes with prompt tool instructions and skill guidance.
49
- - Respect intervention flow for long-running, external, or user-visible operations.
50
-
51
- ## Verification
52
-
53
- - Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
54
- - Related tests observed by source search:
55
- - `tests/test_browser_agent_regressions.py`
56
- - `tests/test_host_browser_connector.py`
57
- - `tests/test_office_desktop_state.py`
58
- - `tests/test_vision_load_image_refs.py`
59
-
60
- ## Child DOX Index
61
-
62
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:826f3b31ec512db1d099dee2cc93ab15933e7699bf7749102f4efdcd7be5c374
3
+ size 4459
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/wait.py CHANGED
@@ -1,89 +1,3 @@
1
- import asyncio
2
- from datetime import timedelta
3
- from helpers.tool import Tool, Response
4
- from helpers.print_style import PrintStyle
5
- from helpers.wait import managed_wait
6
- from helpers.localization import Localization
7
-
8
- class WaitTool(Tool):
9
-
10
- async def execute(self, **kwargs) -> Response:
11
- await self.agent.handle_intervention()
12
-
13
- seconds = self.args.get("seconds", 0)
14
- minutes = self.args.get("minutes", 0)
15
- hours = self.args.get("hours", 0)
16
- days = self.args.get("days", 0)
17
- until_timestamp_str = self.args.get("until")
18
-
19
- is_duration_wait = not bool(until_timestamp_str)
20
-
21
- now = Localization.get().now()
22
- target_time = None
23
-
24
- if until_timestamp_str:
25
- try:
26
- target_time = Localization.get().localtime_str_to_utc_dt(until_timestamp_str)
27
- if not target_time:
28
- raise ValueError(f"Invalid timestamp format: {until_timestamp_str}")
29
- except ValueError as e:
30
- return Response(
31
- message=str(e),
32
- break_loop=False,
33
- )
34
- else:
35
- wait_duration = timedelta(
36
- days=int(days),
37
- hours=int(hours),
38
- minutes=int(minutes),
39
- seconds=int(seconds),
40
- )
41
- if wait_duration.total_seconds() <= 0:
42
- return Response(
43
- message="Wait duration must be positive.",
44
- break_loop=False,
45
- )
46
- target_time = now + wait_duration
47
-
48
- if target_time <= now:
49
- return Response(
50
- message=f"Target time {Localization.get().serialize_datetime(target_time)} is in the past.",
51
- break_loop=False,
52
- )
53
-
54
- PrintStyle.info(f"Waiting until {Localization.get().serialize_datetime(target_time)}...")
55
-
56
- target_time = await managed_wait(
57
- agent=self.agent,
58
- target_time=target_time,
59
- is_duration_wait=is_duration_wait,
60
- log=self.log,
61
- get_heading_callback=self.get_heading
62
- )
63
-
64
- if self.log:
65
- self.log.update(heading=self.get_heading("Done", done=True))
66
-
67
- message = self.agent.read_prompt(
68
- "fw.wait_complete.md",
69
- target_time=Localization.get().serialize_datetime(target_time)
70
- )
71
-
72
- return Response(
73
- message=message,
74
- break_loop=False,
75
- )
76
-
77
- def get_log_object(self):
78
- return self.agent.context.log.log(
79
- type="progress",
80
- heading=self.get_heading(),
81
- content="",
82
- kvps=self.args,
83
- )
84
-
85
- def get_heading(self, text: str = "", done: bool = False):
86
- done_icon = " icon://done_all" if done else ""
87
- if not text:
88
- text = f"Waiting..."
89
- return f"icon://timer Wait: {text}{done_icon}"
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:72989a7fece9f2aa74316f2964c56d996810ff1e08324101f80a599f04cc7fba
3
+ size 2930
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/wait.py.dox.md CHANGED
@@ -1,54 +1,3 @@
1
- # wait.py DOX
2
-
3
- ## Purpose
4
-
5
- - Own the `wait.py` agent tool.
6
- - This module lets the agent wait for a managed duration while respecting intervention flow.
7
- - Keep this file-level DOX profile synchronized with `wait.py` because this directory is intentionally flat.
8
-
9
- ## Ownership
10
-
11
- - `wait.py` owns the runtime implementation.
12
- - `wait.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
13
- - Classes:
14
- - `WaitTool` (`Tool`)
15
- - `async execute(self, **kwargs) -> Response`
16
- - `get_log_object(self)`
17
- - `get_heading(self, text: str=..., done: bool=...)`
18
-
19
- ## Runtime Contracts
20
-
21
- - Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
22
- - Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
23
- - `WaitTool` is a `Tool`.
24
- - `WaitTool` defines `execute(...)`.
25
- - Observed side-effect areas: settings/state persistence.
26
- - Imported dependency areas include: `asyncio`, `datetime`, `helpers.localization`, `helpers.print_style`, `helpers.tool`, `helpers.wait`.
27
-
28
- ## Key Concepts
29
-
30
- - Important called helpers/classes observed in the source: `Localization.get.now`, `PrintStyle.info`, `self.agent.read_prompt`, `Response`, `self.agent.context.log.log`, `self.agent.handle_intervention`, `timedelta`, `managed_wait`, `Localization.get.localtime_str_to_utc_dt`, `wait_duration.total_seconds`, `Localization.get.serialize_datetime`, `self.get_heading`, `ValueError`.
31
- - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
32
-
33
- ## Work Guidance
34
-
35
- - Keep tool output concise, model-readable, and safe for history persistence.
36
- - Coordinate argument or behavior changes with prompt tool instructions and skill guidance.
37
- - Respect intervention flow for long-running, external, or user-visible operations.
38
-
39
- ## Verification
40
-
41
- - Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
42
- - Related tests observed by source search:
43
- - `tests/email_parser_test.py`
44
- - `tests/rate_limiter_test.py`
45
- - `tests/test_api_chat_lifetime.py`
46
- - `tests/test_browser_agent_regressions.py`
47
- - `tests/test_chat_compaction.py`
48
- - `tests/test_default_prompt_budget.py`
49
- - `tests/test_document_query_plugin.py`
50
- - `tests/test_download_toast_regressions.py`
51
-
52
- ## Child DOX Index
53
-
54
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7de7c43d3c40bdf266841d4c554d15e759b883f4b066dd758b4b7afa078558d1
3
+ size 2477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
update_reqs.py CHANGED
@@ -1,38 +1,3 @@
1
- import pkg_resources
2
- import re
3
-
4
- def get_installed_version(package_name):
5
- try:
6
- return pkg_resources.get_distribution(package_name).version
7
- except pkg_resources.DistributionNotFound:
8
- return None
9
-
10
- def update_requirements():
11
- with open('requirements.txt', 'r') as f:
12
- requirements = f.readlines()
13
-
14
- updated_requirements = []
15
- for req in requirements:
16
- req = req.strip()
17
- if not req or req.startswith('#'):
18
- updated_requirements.append(req)
19
- continue
20
-
21
- # Extract package name
22
- match = re.match(r'^([^=<>]+)==', req)
23
- if match:
24
- package_name = match.group(1)
25
- current_version = get_installed_version(package_name)
26
- if current_version:
27
- updated_requirements.append(f'{package_name}=={current_version}')
28
- else:
29
- updated_requirements.append(req) # Keep original if package not found
30
- else:
31
- updated_requirements.append(req) # Keep original if pattern doesn't match
32
-
33
- # Write updated requirements
34
- with open('requirements.txt', 'w') as f:
35
- f.write('\n'.join(updated_requirements) + '\n')
36
-
37
- if __name__ == '__main__':
38
- update_requirements()
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d6f700fc5dfbb40827732adb650e07b080cf5796443849f3acd1a81be1556968
3
+ size 1248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
usr/agents/.gitkeep ADDED
File without changes
usr/knowledge/.gitkeep ADDED
File without changes
usr/knowledge/main/.gitkeep ADDED
File without changes
usr/knowledge/solutions/.gitkeep ADDED
File without changes
usr/plugins/.gitkeep ADDED
File without changes
usr/skills/.gitkeep ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
3
+ size 1
usr/workdir/.gitkeep ADDED
File without changes
webui/AGENTS.md CHANGED
@@ -1,60 +1,3 @@
1
- # WebUI DOX
2
-
3
- ## Purpose
4
-
5
- - Own the Flask-served Alpine.js WebUI shell, frontend modules, components, CSS, static assets, and vendored browser libraries.
6
- - Keep the UI coherent with backend APIs, WebSocket state, plugin extension points, and documented frontend patterns.
7
-
8
- ## Ownership
9
-
10
- - `splash.html` owns the self-contained cache/bootstrap document served at `/`; `safe.html` owns the self-contained service-worker escape hatch served at `/safe`; `index.html`, `index.js`, and `index.css` define the rendered main UI shell served directly at `/index.html` and `/safe`, and fetched from `/ui/index` for in-place installation at `/`.
11
- - `components/` owns self-contained Alpine components and component stores.
12
- - `js/` owns shared frontend modules, API clients, WebSocket clients, stores, extension loaders, and utility code.
13
- - `css/` owns shared stylesheet modules.
14
- - `public/` owns first-party static image/icon assets.
15
- - `vendor/` owns vendored third-party browser libraries.
16
-
17
- ## Local Contracts
18
-
19
- - Store-dependent UI must be gated with `x-data` and `x-if="$store.storeName"` before using the store.
20
- - Use `createStore` from `/js/AlpineStore.js` for frontend stores.
21
- - Use `openModal(path)` and `closeModal()` from `/js/modals.js` for modal flows.
22
- - Use `/js/api.js` helpers so CSRF and auth behavior stays consistent.
23
- - Component tags use `<x-component path="...">`; paths are resolved under `webui/components/` when not already prefixed.
24
- - Frontend extension breakpoints use `<x-extension id="...">` and are loaded through `/js/extensions.js`.
25
- - `sw.js` owns same-origin HTML/CSS/JavaScript caching. The self-contained splash fetches `/ui/index` and the gzip-compressed `/ui/asset-bundle` in parallel, installs the versioned worker, waits until the bundle is usable in memory, and then installs the rendered index into the current `/` document without navigation. The worker persists the whole prepared bundle as one Cache Storage response in the background and removes obsolete version caches. Eligible text bodies up to 512 KiB each may be embedded; unknown or runtime-computed text URLs use one ordinary backend request and are cached as the native response when their transferred body is at most 256 KiB. The worker does not parse imports, call a secondary graph endpoint, or perform compression. Worker, cache, bundle, and timeout failures fall through to the already-started backend document or ordinary backend asset request. Media, images, fonts, and manifests use normal browser HTTP caching.
26
- - `splash.html` keeps its logo, styling, and bootstrap logic self-contained. Before replacing the document, it carries its current overlay markup into the rendered index. `index.html` owns the matching inline critical overlay style and fades that same visual overlay after initial component/extension readiness, with a bounded failure timeout; it must not contain duplicate static overlay markup. Icon-font preloading begins when the index is placed behind the overlay, continues independently behind square transparent placeholders, and must neither compete with the startup bundle nor delay the application reveal. `safe.html` must remain self-contained: its first phase unregisters all service workers for the origin, then replaces itself with the directly rendered `/safe` phase without fetching the bundle or registering a worker. The direct phase repeats unregistration defensively but does not initialize a worker; returning to `/` may install the current worker again. The rendered index removes only the internal safe-mode query marker from `/safe` in browser history. Except for the tiny blocking icon guard stylesheet needed to prevent ligature layout shifts, initial application stylesheets remain non-render-blocking.
27
- - `js/icons.js` owns the native `<x-icon>` element. First-party WebUI and bundled-plugin markup must use `<x-icon name="lowercase_snake_case"></x-icon>` for static Material Symbols and `:name="expression"` for Alpine-driven names; do not put ligature text inside the element or author new `.material-symbols-outlined` spans. The element uses the single local WOFF2 font rather than per-icon SVG requests, validates names, defaults unlabeled icons to `aria-hidden="true"`, and retains the legacy Material class internally so established selectors continue to apply. The shared vendor CSS constrains both `<x-icon>` and legacy ligature spans to a clipped 1:1 em square and keeps them transparent until the icon face is confirmed after the installed application document finishes parsing. `.material-symbols-outlined` and `.material-icons-outlined` spans remain supported for third-party plugin compatibility but are not the first-party authoring API.
28
- - Component HTML loaded by the shared loader may include `<title>`, module scripts, body content, and scoped styles; modal content uses the same loader path.
29
- - Do not bypass WebSocket origin/auth/CSRF assumptions from frontend code.
30
- - Avoid editing vendored files unless intentionally updating the vendor asset.
31
- - Startup scripts in `index.html` must be local and non-parser-blocking: use ES modules, `defer`, or `async` for every script with `src`.
32
- - Baseline main and login pages must load scripts, styles, fonts, and images from same-origin vendored or first-party assets so they remain usable without internet access.
33
- - Rubik (`--font-family-main`) is the default WebUI text and control font; use the code/mono font tokens only for code, logs, paths, and fixed-width data.
34
- - Hover, focus, and active border treatments should follow existing neutral border/background patterns; avoid hard-coded blue border highlights unless matching an established specialized surface.
35
- - Keep transient UI affordances such as Bootstrap tooltips and notification toasts above normal and legacy modal layers while confirmation dialogs remain on top.
36
-
37
- ## Work Guidance
38
-
39
- - Put component-specific markup and styles under `components/`; put reusable frontend infrastructure under `js/`; put shared visual primitives under `css/`.
40
- - Keep UI text and controls consistent with existing components.
41
- - Use notifications for user-facing success, warning, and error feedback where the app already uses notification flows.
42
- - Coordinate API payload changes with backend handlers and tests.
43
-
44
- ## Verification
45
-
46
- - Run targeted WebUI/frontend tests when available.
47
- - Manually smoke-test visible UI changes with `python run_ui.py` when behavior cannot be covered by tests.
48
- - Verify desktop and mobile layout for substantial UI changes.
49
-
50
- ## Child DOX Index
51
-
52
- Direct child DOX files:
53
-
54
- | Child | Scope |
55
- | --- | --- |
56
- | [components/AGENTS.md](components/AGENTS.md) | Alpine component HTML, component stores, and component-local styles. |
57
- | [css/AGENTS.md](css/AGENTS.md) | Shared WebUI stylesheet modules. |
58
- | [js/AGENTS.md](js/AGENTS.md) | Shared frontend JavaScript modules and client-side infrastructure. |
59
- | [public/AGENTS.md](public/AGENTS.md) | First-party static WebUI images, icons, splash art, and PWA assets. |
60
- | [vendor/AGENTS.md](vendor/AGENTS.md) | Vendored third-party browser libraries. |
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:905c7dbc80e8df70a9d275d78506c5f88673fff8a686562d3f4566b69eddc22c
3
+ size 7052
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/AGENTS.md CHANGED
@@ -1,77 +1,3 @@
1
- # WebUI Components DOX
2
-
3
- ## Purpose
4
-
5
- - Own self-contained Alpine.js components, component stores, and component-local styles.
6
- - Keep component lifecycle, store registration, and nested component loading consistent.
7
-
8
- ## Ownership
9
-
10
- - Each subdirectory owns a UI feature area such as sidebar, settings, projects, notifications, plugins, canvas, or welcome screens.
11
- - `*-store.js` files own component state and actions.
12
- - Component HTML files own their markup, local module imports, and local scoped styles.
13
- - `_examples/` contains reference implementations for component and store shape.
14
- - Modal components normally live under `modals/<name>/<name>.html`; settings components live under `settings/`.
15
-
16
- ## Local Contracts
17
-
18
- - Module imports belong in the component `<head>`.
19
- - Module scripts must use `type="module"` and import stores before Alpine evaluates bindings.
20
- - Rendered content belongs in the component `<body>`.
21
- - Wrap store-dependent content with `x-data` and a `template x-if="$store.name"` gate.
22
- - A gated `<template>` must contain one root element.
23
- - Keep component-specific styles in the component `<style>` block; use shared CSS only for primitives that are genuinely reused.
24
- - Use CSS variables from `index.css` for color, spacing, typography, borders, and transitions.
25
- - Use `x-create` for per-mount setup and `x-destroy` for cleanup; do not call long-lived store `init()` from `x-init`.
26
- - Store `init()` must be idempotent and guarded when it registers global listeners, intervals, or one-time data.
27
- - Store state can be read as `$store.name` in templates and through direct module imports in JavaScript; avoid `window.Alpine.store()` lookups in component code.
28
- - Use `globalThis.xAttrs(element)` only for explicit parent `<x-component>` attribute inheritance.
29
- - Use shared API, modal, notification, confirmation, and store helpers instead of ad hoc globals.
30
- - Author Material Symbols as empty `<x-icon name="lowercase_snake_case"></x-icon>` elements. Bind changing icons through `:name`, keep accessible text on the owning control, and do not add new ligature-text spans or `x-text` icon bindings.
31
- - Components that use polling directives may use `x-every-second`, `x-every-minute`, or `x-every-hour` only while mounted.
32
- - Use `$confirmClick` for destructive two-click confirmations where the existing UI pattern fits.
33
- - Name component files `feature-name.html` and stores `feature-store.js` or `feature-name-store.js`.
34
-
35
- ## Work Guidance
36
-
37
- - Keep component stores cohesive and avoid cross-component state mutation unless a shared store owns that state.
38
- - Prefer nested `<x-component path="...">` for reusable UI pieces.
39
- - Use absolute imports such as `/components/...` and `/js/...` so loader-generated module URLs resolve reliably.
40
- - Preserve flex layouts through loader wrappers with `display: contents` on the specific wrapper chain when needed.
41
- - Use `callJsonApi()` for JSON-in/JSON-out calls and `fetchApi()` for raw fetches that still need CSRF handling.
42
- - Use `openModal(path)` for modal entry points; modal titles come from component `<title>`.
43
- - Add modal footers with `data-modal-footer` so `modals.js` can move them to the pinned footer slot.
44
- - Keep modal footer elements stable after load; conditional footers should not be repeatedly created and destroyed.
45
- - Use `.modal-floating` only for non-blocking utility panels where page clicks must pass through; do not use it for destructive confirmations, settings, auth, import/export, or required workflows.
46
- - Do not add `document.addEventListener("alpine:init", ...)` blocks inside component HTML.
47
- - Do not use legacy `x-teleport` plus `.modal-overlay` modal patterns for new work.
48
- - Do not introduce a second modal overlay system or manually reshape `.modal-inner`, `.modal-scroll`, or `.modal-footer-slot`.
49
- - Do not hide fetch, import, or lifecycle errors with broad `.catch(() => null)` handlers; surface failures through notifications or console errors as appropriate.
50
- - Do not store sensitive modal values in long-lived stores without explicit cleanup on close.
51
-
52
- ## Verification
53
-
54
- - Manually exercise the affected component in the WebUI for visible or lifecycle changes.
55
- - Run targeted tests for settings, plugins, notifications, projects, state sync, or other touched flows when available.
56
- - For modal changes, verify open, close button, Escape, click-outside, stacked modal, scroll, and pinned footer behavior.
57
-
58
- ## Child DOX Index
59
-
60
- Direct child DOX files:
61
-
62
- | Child | Scope |
63
- | --- | --- |
64
- | [_examples/AGENTS.md](_examples/AGENTS.md) | Reference component and store examples. |
65
- | [canvas/AGENTS.md](canvas/AGENTS.md) | Right-canvas component surface and store. |
66
- | [chat/AGENTS.md](chat/AGENTS.md) | Chat composer, attachments, queue, navigation, and top-section components. |
67
- | [dropdown/AGENTS.md](dropdown/AGENTS.md) | Shared dropdown component. |
68
- | [messages/AGENTS.md](messages/AGENTS.md) | Message rendering helpers, action buttons, process groups, and resize behavior. |
69
- | [modals/AGENTS.md](modals/AGENTS.md) | Modal component workflows loaded through the shared modal stack. |
70
- | [notifications/AGENTS.md](notifications/AGENTS.md) | Toasts, notification modal, icons, and notification store. |
71
- | [plugins/AGENTS.md](plugins/AGENTS.md) | Plugin settings, info, list, execution, and toggle components. |
72
- | [projects/AGENTS.md](projects/AGENTS.md) | Project creation, selection, edit, secrets, model, skill, and file-structure components. |
73
- | [settings/AGENTS.md](settings/AGENTS.md) | Settings shell and built-in settings subsections. |
74
- | [sidebar/AGENTS.md](sidebar/AGENTS.md) | Left sidebar layout, chat/task lists, top actions, and preferences. |
75
- | [sync/AGENTS.md](sync/AGENTS.md) | WebUI sync status component and store. |
76
- | [tooltips/AGENTS.md](tooltips/AGENTS.md) | Shared tooltip store and behavior. |
77
- | [welcome/AGENTS.md](welcome/AGENTS.md) | Welcome screen, discovery cards, banners, and welcome state. |
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b4ee454a09ecc7546e8c759ed222b279208ee9cc5f053beb3e5c0ae352d48c7
3
+ size 5902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/_examples/AGENTS.md CHANGED
@@ -1,27 +1,3 @@
1
- # Component Examples DOX
2
-
3
- ## Purpose
4
-
5
- - Own reference component and store examples for WebUI component authors.
6
-
7
- ## Ownership
8
-
9
- - `_example-component.html` owns the component markup example.
10
- - `_example-store.js` owns the store pattern example.
11
-
12
- ## Local Contracts
13
-
14
- - Keep examples aligned with current component loader, Store Gating, and `createStore` patterns.
15
- - Do not add product behavior or dependencies here.
16
-
17
- ## Work Guidance
18
-
19
- - Prefer small examples that demonstrate current conventions clearly.
20
-
21
- ## Verification
22
-
23
- - Manually inspect examples after component contract changes.
24
-
25
- ## Child DOX Index
26
-
27
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b7033487bae6a3700601a3a13ee754d764cae76c0175a4539b12c961557b803
3
+ size 624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/_examples/_example-component.html CHANGED
@@ -1,36 +1,3 @@
1
- <html>
2
-
3
- <head>
4
- <title>Example component or modal</title>
5
-
6
- <!-- Import the alpine store -->
7
- <script type="module">
8
- import { store } from "/components/_examples/_example-store.js";
9
- </script>
10
- </head>
11
-
12
- <body>
13
-
14
- <!-- This construct of x-data + x-if is used to ensure the component is only rendered when the store is available -->
15
- <div x-data>
16
- <template x-if="$store.exampleStore">
17
-
18
- <!-- Keep in mind that <template> can have only one root element inside -->
19
- <div>
20
- <p x-text="$store.exampleStore.example1"></p>
21
- <p x-text="$store.exampleStore.example2"></p>
22
- </div>
23
-
24
- </template>
25
- </div>
26
-
27
- <!-- Optional style for the component -->
28
- <style>
29
- #example-component {
30
- width: 100%;
31
- }
32
- </style>
33
-
34
- </body>
35
-
36
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:004c8364524a2809211ce97806aafbfe357cd1e283a025208f746d5a6ab82f98
3
+ size 862
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/_examples/_example-store.js CHANGED
@@ -1,23 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
-
3
- // define the model object holding data and functions
4
- const model = {
5
- example1:"Example 1",
6
- example2:"Example 2",
7
-
8
- // gets called when the store is created
9
- init(){
10
- console.log("Example store initialized");
11
- },
12
-
13
- clickHandler(event){
14
- console.log(event)
15
- }
16
-
17
- };
18
-
19
- // convert it to alpine store
20
- const store = createStore("_exampleStore", model);
21
-
22
- // export for use in other files
23
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc4574407b5841fdcc2e14f85c0272c3f96770e7c666470d848c6dad0bb66769
3
+ size 485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/canvas/AGENTS.md CHANGED
@@ -1,33 +1,3 @@
1
- # Canvas Components DOX
2
-
3
- ## Purpose
4
-
5
- - Own the right-canvas component surface and its state.
6
-
7
- ## Ownership
8
-
9
- - `right-canvas.html` owns canvas markup.
10
- - `right-canvas-store.js` owns canvas state, surface registration, and actions.
11
- - `right-canvas.css` owns canvas-specific styling.
12
-
13
- ## Local Contracts
14
-
15
- - Keep registered surfaces compatible with WebUI extension hooks.
16
- - Preserve responsive layout and avoid overlapping the chat/sidebar shells.
17
- - Right-canvas rail and tab buttons are explicit canvas entry points above the mobile breakpoint; at mobile widths, keep the rail visible but route non-action surfaces into floating modals instead of the side canvas shell.
18
- - Keep the right-canvas rail and docked shell hidden while the welcome screen is active; non-action surface opens during welcome must route into floating/modal surfaces instead of docking beside the welcome screen.
19
- - Preserve docked canvas open state across same-tab reloads with session-scoped state, but do not treat it as durable cross-session UI state.
20
- - In mobile mode, keep the rail below blocking modal layers and compact it on very narrow screens instead of letting it cover modal content.
21
- - Keep the canvas shell functional when the instance-level visibility preference hides only the right-canvas rail.
22
-
23
- ## Work Guidance
24
-
25
- - Use existing surface and extension helpers before adding new canvas infrastructure.
26
-
27
- ## Verification
28
-
29
- - Smoke-test right-canvas open, close, resize, and registered surfaces after changes.
30
-
31
- ## Child DOX Index
32
-
33
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1cfb44c6adfd734461e1d51790c5b79bf01ea4a67cf269dc63f32b905ae88574
3
+ size 1530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/canvas/right-canvas-store.js CHANGED
@@ -1,620 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
3
- import { callJsExtensions } from "/js/extensions.js";
4
- import {
5
- SURFACE_MODE_DOCKED,
6
- SURFACE_MODE_FLOATING,
7
- closeSurfaceGroupModals,
8
- getRegisteredSurfaces,
9
- migratePersistedSurfaceState,
10
- normalizeSurfaceId,
11
- normalizeSurfaceMode,
12
- registerSurface as registerSurfaceDefinition,
13
- } from "/js/surfaces.js";
14
-
15
- const STORAGE_KEY = "a0.rightCanvas";
16
- const SESSION_STORAGE_KEY = "a0.rightCanvas.session";
17
- const DEFAULT_WIDTH = 720;
18
- const MIN_WIDTH = 0;
19
- const DESKTOP_BREAKPOINT = 1200;
20
- const MOBILE_BREAKPOINT = 768;
21
-
22
- function clamp(value, min, max) {
23
- return Math.min(Math.max(value, min), max);
24
- }
25
-
26
- function viewportWidth() {
27
- return Math.max(document.documentElement.clientWidth || 0, globalThis.innerWidth || 0);
28
- }
29
-
30
- function normalizeWidth(value, fallback = DEFAULT_WIDTH) {
31
- if (value === null || value === undefined || value === "") return fallback;
32
- const width = Number(value);
33
- return Number.isFinite(width) ? Math.max(MIN_WIDTH, Math.round(width)) : fallback;
34
- }
35
-
36
- function isReloadNavigation() {
37
- const navigation = performance.getEntriesByType?.("navigation")?.[0];
38
- if (navigation?.type) return navigation.type === "reload";
39
- if (!performance.navigation) return false;
40
- return performance.navigation?.type === performance.navigation?.TYPE_RELOAD;
41
- }
42
-
43
- const model = {
44
- surfaces: [],
45
- activeSurfaceId: "",
46
- surfaceModes: {},
47
- mountedSurfaces: {},
48
- isOpen: false,
49
- width: DEFAULT_WIDTH,
50
- isOverlayMode: false,
51
- isMobileMode: false,
52
- _initialized: false,
53
- _registering: false,
54
- _rootElement: null,
55
- _resizeCleanup: null,
56
- _lastPayloadBySurface: {},
57
- _restoreOpenRequested: false,
58
- _restoreOpenTimer: null,
59
- _restoringSurface: false,
60
-
61
- async init(element = null) {
62
- if (element) this._rootElement = element;
63
- if (this._initialized) {
64
- this.applyLayoutState();
65
- this.scheduleRestoreOpenSurface();
66
- return;
67
- }
68
-
69
- this._initialized = true;
70
- this.restore();
71
- this.updateLayoutMode();
72
- this.applyLayoutState();
73
- globalThis.addEventListener("resize", () => {
74
- this.updateLayoutMode();
75
- this.setWidth(this.width, { persist: false });
76
- this.applyLayoutState();
77
- });
78
-
79
- if (!this._registering) {
80
- this._registering = true;
81
- await callJsExtensions("surfaces_register", this);
82
- await callJsExtensions("right_canvas_register_surfaces", this);
83
- this._registering = false;
84
- this.ensureActiveSurface();
85
- this.scheduleRestoreOpenSurface();
86
- }
87
- },
88
-
89
- registerSurface(surface) {
90
- if (!surface?.id) return;
91
- const surfaceId = normalizeSurfaceId(surface.id);
92
- const normalized = {
93
- title: surface.id,
94
- icon: "web_asset",
95
- image: "",
96
- order: 100,
97
- canOpen: () => true,
98
- open: () => {},
99
- close: () => {},
100
- modalPath: "",
101
- actionOnly: false,
102
- ...surface,
103
- id: surfaceId,
104
- };
105
-
106
- const index = this.surfaces.findIndex((item) => item.id === normalized.id);
107
- if (index >= 0) {
108
- this.surfaces.splice(index, 1, normalized);
109
- } else {
110
- this.surfaces.push(normalized);
111
- }
112
- if (!this.surfaceModes[normalized.id]) {
113
- this.surfaceModes[normalized.id] = SURFACE_MODE_DOCKED;
114
- }
115
- registerSurfaceDefinition(normalized);
116
- this.surfaces.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
117
- if (!this._registering) {
118
- this.ensureActiveSurface();
119
- }
120
- },
121
-
122
- ensureActiveSurface() {
123
- const panelSurfaces = this.panelSurfaces;
124
- if (!panelSurfaces.length) {
125
- this.activeSurfaceId = "";
126
- return;
127
- }
128
- if (!panelSurfaces.some((surface) => surface.id === this.activeSurfaceId)) {
129
- this.activeSurfaceId = panelSurfaces[0].id;
130
- }
131
- },
132
-
133
- async open(surfaceId = "", payload = {}) {
134
- const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
135
- const surface = this.getSurface(targetId);
136
- if (!surface) {
137
- return false;
138
- }
139
- if (typeof surface.canOpen === "function" && surface.canOpen(payload) === false) {
140
- return false;
141
- }
142
-
143
- this._restoreOpenRequested = false;
144
- this.cancelRestoreOpenSurface();
145
-
146
- if (surface.actionOnly) {
147
- try {
148
- await surface.open?.(payload || {});
149
- } catch (error) {
150
- console.error(`Canvas action ${targetId} failed`, error);
151
- }
152
- return true;
153
- }
154
-
155
- if (!this.shouldRender()) {
156
- return await this.openModalSurface(targetId, payload);
157
- }
158
-
159
- if (this.isMobileMode) {
160
- this.activeSurfaceId = targetId;
161
- this.isOpen = false;
162
- return await this.openModalSurface(targetId, payload);
163
- }
164
-
165
- this.activeSurfaceId = targetId;
166
- this.markSurfaceMounted(targetId);
167
- this.isOpen = true;
168
- this.recordSurfaceMode(targetId, SURFACE_MODE_DOCKED, { persist: false });
169
- this._lastPayloadBySurface[targetId] = payload || {};
170
- this.persist();
171
- this.applyLayoutState();
172
-
173
- try {
174
- await surface.open?.(payload || {});
175
- } catch (error) {
176
- console.error(`Canvas surface ${targetId} failed to open`, error);
177
- }
178
- return true;
179
- },
180
-
181
- markSurfaceMounted(surfaceId) {
182
- const targetId = normalizeSurfaceId(surfaceId);
183
- if (!targetId) return;
184
- this.mountedSurfaces = {
185
- ...this.mountedSurfaces,
186
- [targetId]: true,
187
- };
188
- },
189
-
190
- markSurfaceUnmounted(surfaceId) {
191
- const targetId = normalizeSurfaceId(surfaceId);
192
- if (!targetId || !this.mountedSurfaces[targetId]) return;
193
- const next = { ...this.mountedSurfaces };
194
- delete next[targetId];
195
- this.mountedSurfaces = next;
196
- },
197
-
198
- mountedSurfaceIds() {
199
- return Object.entries(this.mountedSurfaces)
200
- .filter(([, mounted]) => mounted)
201
- .map(([surfaceId]) => surfaceId);
202
- },
203
-
204
- isSurfaceMounted(id) {
205
- return Boolean(this.mountedSurfaces[normalizeSurfaceId(id)]);
206
- },
207
-
208
- isSurfaceRendered(id) {
209
- return Boolean(this.isOpen && this.isSurfaceMounted(id));
210
- },
211
-
212
- isSurfaceVisible(id) {
213
- const targetId = normalizeSurfaceId(id);
214
- return Boolean(this.isOpen && this.activeSurfaceId === targetId && this.isSurfaceMounted(targetId));
215
- },
216
-
217
- recordSurfaceMode(surfaceId, mode = SURFACE_MODE_DOCKED, options = {}) {
218
- const targetId = normalizeSurfaceId(surfaceId);
219
- if (!targetId) return;
220
- this.surfaceModes = {
221
- ...this.surfaceModes,
222
- [targetId]: normalizeSurfaceMode(mode),
223
- };
224
- if (options.persist !== false) this.persist();
225
- },
226
-
227
- latestSurfaceMode(surfaceId) {
228
- const targetId = normalizeSurfaceId(surfaceId);
229
- return normalizeSurfaceMode(this.surfaceModes[targetId]);
230
- },
231
-
232
- async openLatest(surfaceId = "", payload = {}) {
233
- const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
234
- if (!targetId) return false;
235
- if (this.isMobileMode) {
236
- return await this.open(targetId, payload);
237
- }
238
- if (this.latestSurfaceMode(targetId) === SURFACE_MODE_FLOATING) {
239
- return await this.openModalSurface(targetId, payload);
240
- }
241
- return await this.open(targetId, payload);
242
- },
243
-
244
- async close() {
245
- this._restoreOpenRequested = false;
246
- this.cancelRestoreOpenSurface();
247
- this.isOpen = false;
248
- this.persist();
249
- this.applyLayoutState();
250
- return true;
251
- },
252
-
253
- async dockSurface(surfaceId, payload = {}) {
254
- surfaceId = normalizeSurfaceId(surfaceId);
255
- if (this.isMobileMode) {
256
- return false;
257
- }
258
- const surface = this.getSurface(surfaceId);
259
- if (!surface) {
260
- return false;
261
- }
262
- const modalPath = payload.modalPath || surface.modalPath || "";
263
- let handoffStarted = false;
264
- try {
265
- await surface.beginDockHandoff?.(payload);
266
- handoffStarted = true;
267
-
268
- const closed = await this.closeDockSourceModal(payload, modalPath);
269
- if (closed === false) {
270
- await surface.cancelDockHandoff?.(payload);
271
- return false;
272
- }
273
-
274
- const openPayload = { ...payload, source: "modal" };
275
- delete openPayload.closeSourceModal;
276
- const opened = await this.open(surfaceId, openPayload);
277
- await surface.finishDockHandoff?.({ ...openPayload, opened });
278
- return opened;
279
- } catch (error) {
280
- if (handoffStarted) {
281
- await surface.cancelDockHandoff?.(payload);
282
- }
283
- console.error(`Canvas surface ${surfaceId} failed to dock`, error);
284
- return false;
285
- }
286
- },
287
-
288
- async closeDockSourceModal(payload = {}, modalPath = "") {
289
- if (typeof payload.closeSourceModal === "function") {
290
- return (await payload.closeSourceModal()) !== false;
291
- }
292
-
293
- const sourceModalPath = payload.sourceModalPath || modalPath;
294
- if (sourceModalPath || modalPath) {
295
- const closed = await closeSurfaceGroupModals();
296
- if (closed === false) return false;
297
- if (!sourceModalPath || !globalThis.isModalOpen?.(sourceModalPath)) return true;
298
- }
299
- if (sourceModalPath && globalThis.isModalOpen?.(sourceModalPath)) {
300
- return (await globalThis.closeModal?.(sourceModalPath)) !== false;
301
- }
302
- if (modalPath && modalPath !== sourceModalPath && globalThis.isModalOpen?.(modalPath)) {
303
- return (await globalThis.closeModal?.(modalPath)) !== false;
304
- }
305
- return true;
306
- },
307
-
308
- async undockSurface(surfaceId = "", payload = {}) {
309
- const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId);
310
- const surface = this.getSurface(targetId);
311
- const modalPath = payload.modalPath || surface?.modalPath || "";
312
- if (!surface || !modalPath) return false;
313
- const openModal = globalThis.ensureModalOpen || globalThis.openModal;
314
- if (!openModal) return false;
315
- if (this.activeSurfaceId === targetId) {
316
- this.isOpen = false;
317
- this.persist();
318
- this.applyLayoutState();
319
- }
320
- this.recordSurfaceMode(targetId, SURFACE_MODE_FLOATING);
321
- const modalPromise = openModal(modalPath);
322
- if (modalPromise?.catch) {
323
- modalPromise.catch((error) => console.error(`Canvas surface ${targetId} failed to undock`, error));
324
- }
325
- return true;
326
- },
327
-
328
- async openModalSurface(surfaceId = "", payload = {}) {
329
- const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId);
330
- const surface = this.getSurface(targetId);
331
- const modalPath = payload.modalPath || surface?.modalPath || "";
332
- if (!surface || !modalPath) return false;
333
- const openModal = globalThis.ensureModalOpen || globalThis.openModal;
334
- if (!openModal) return false;
335
-
336
- this._restoreOpenRequested = false;
337
- this.cancelRestoreOpenSurface();
338
-
339
- if (this.isOpen && this.activeSurfaceId === targetId) {
340
- this.isOpen = false;
341
- this.persist();
342
- this.applyLayoutState();
343
- }
344
-
345
- this.recordSurfaceMode(targetId, SURFACE_MODE_FLOATING);
346
- const modalPromise = openModal(modalPath);
347
- if (modalPromise?.catch) {
348
- modalPromise.catch((error) => console.error(`Canvas surface ${targetId} failed to open as modal`, error));
349
- }
350
- return true;
351
- },
352
-
353
- async undockActiveSurface() {
354
- return await this.undockSurface(this.activeSurfaceId);
355
- },
356
-
357
- currentSurfaceCanUndock() {
358
- return Boolean(this.currentSurface()?.modalPath);
359
- },
360
-
361
- async toggle(surfaceId = "", payload = {}) {
362
- const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
363
- if (this.isOpen && targetId === this.activeSurfaceId) {
364
- await this.close();
365
- return false;
366
- }
367
- return await this.open(targetId, payload);
368
- },
369
-
370
- async toggleCanvas() {
371
- if (!this.shouldRender()) return false;
372
- if (this.isMobileMode) {
373
- return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "", { source: "mobile-toggle" });
374
- }
375
- if (this.isOpen) {
376
- await this.close();
377
- return false;
378
- }
379
- return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
380
- },
381
-
382
- setWidth(px, options = {}) {
383
- const { persist = true } = options;
384
- const next = clamp(normalizeWidth(px), MIN_WIDTH, this.maxWidth());
385
- this.width = next;
386
- this.applyLayoutState();
387
- if (persist) this.persist();
388
- },
389
-
390
- maxWidth() {
391
- if (this.isOverlayMode) {
392
- return Math.max(MIN_WIDTH, viewportWidth() - 44);
393
- }
394
-
395
- const container = this._rootElement?.closest(".container");
396
- const rightPanel = document.getElementById("right-panel");
397
- const containerRight = container?.getBoundingClientRect().right ?? viewportWidth();
398
- const panelLeft = rightPanel?.getBoundingClientRect().left ?? 0;
399
- return Math.max(MIN_WIDTH, Math.floor(containerRight - panelLeft));
400
- },
401
-
402
- defaultWidth() {
403
- return Math.min(DEFAULT_WIDTH, Math.floor(viewportWidth() * 0.45));
404
- },
405
-
406
- startResize(event) {
407
- if (this.isOverlayMode || this.isMobileMode || !this.isOpen) return;
408
- if (event.button !== 0) return;
409
- event.preventDefault();
410
- this.dispatchResizeEvent("right-canvas-resize-start");
411
-
412
- const onPointerMove = (moveEvent) => {
413
- const nextWidth = viewportWidth() - moveEvent.clientX;
414
- this.setWidth(nextWidth);
415
- };
416
- const onPointerUp = () => {
417
- globalThis.removeEventListener("pointermove", onPointerMove);
418
- globalThis.removeEventListener("pointerup", onPointerUp);
419
- globalThis.removeEventListener("pointercancel", onPointerUp);
420
- document.body.classList.remove("right-canvas-resizing");
421
- this.persist();
422
- this.dispatchResizeEvent("right-canvas-resize-end");
423
- };
424
-
425
- document.body.classList.add("right-canvas-resizing");
426
- globalThis.addEventListener("pointermove", onPointerMove);
427
- globalThis.addEventListener("pointerup", onPointerUp);
428
- globalThis.addEventListener("pointercancel", onPointerUp);
429
- },
430
-
431
- dispatchResizeEvent(name) {
432
- try {
433
- globalThis.dispatchEvent(new CustomEvent(name, {
434
- detail: {
435
- width: this.width,
436
- activeSurfaceId: this.activeSurfaceId,
437
- },
438
- }));
439
- } catch {
440
- // Resize events are an optimization hook for embedded surfaces.
441
- }
442
- },
443
-
444
- cancelRestoreOpenSurface() {
445
- if (!this._restoreOpenTimer) return;
446
- globalThis.clearTimeout?.(this._restoreOpenTimer);
447
- this._restoreOpenTimer = null;
448
- },
449
-
450
- scheduleRestoreOpenSurface() {
451
- if (!this._restoreOpenRequested || this._restoreOpenTimer) return;
452
- if (!this.shouldRender() || this.isMobileMode) return;
453
- this._restoreOpenTimer = globalThis.setTimeout?.(() => {
454
- this._restoreOpenTimer = null;
455
- this.restoreOpenSurface();
456
- }, 0) || null;
457
- },
458
-
459
- async restoreOpenSurface() {
460
- if (!this._restoreOpenRequested || this._restoringSurface) return false;
461
- if (!this.shouldRender() || this.isMobileMode) {
462
- return false;
463
- }
464
-
465
- const targetId = normalizeSurfaceId(this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
466
- if (!targetId || !this.getSurface(targetId)) {
467
- return false;
468
- }
469
-
470
- this._restoreOpenRequested = false;
471
- this._restoringSurface = true;
472
- try {
473
- return await this.open(targetId, { source: "reload-restore" });
474
- } finally {
475
- this._restoringSurface = false;
476
- }
477
- },
478
-
479
- persist() {
480
- const state = {
481
- isOpen: this.isOpen,
482
- activeSurfaceId: this.activeSurfaceId,
483
- surfaceModes: this.surfaceModes,
484
- width: this.width,
485
- };
486
- try {
487
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
488
- } catch (error) {
489
- console.warn("Could not persist right canvas state", error);
490
- }
491
- try {
492
- sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));
493
- } catch (error) {
494
- console.warn("Could not persist right canvas session state", error);
495
- }
496
- },
497
-
498
- restore() {
499
- this.width = this.defaultWidth();
500
- try {
501
- const saved = migratePersistedSurfaceState(JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"));
502
- this.isOpen = false;
503
- this.activeSurfaceId = String(saved.activeSurfaceId || "");
504
- this.surfaceModes = Object.fromEntries(
505
- Object.entries(saved.surfaceModes || {}).map(([surfaceId, mode]) => [
506
- surfaceId,
507
- normalizeSurfaceMode(mode),
508
- ]),
509
- );
510
- if (Number.isFinite(Number(saved.width))) this.width = Number(saved.width);
511
- } catch (error) {
512
- console.warn("Could not restore right canvas state", error);
513
- }
514
-
515
- if (isReloadNavigation()) {
516
- try {
517
- const savedSession = migratePersistedSurfaceState(
518
- JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY) || "{}"),
519
- );
520
- if (savedSession?.activeSurfaceId) this.activeSurfaceId = String(savedSession.activeSurfaceId);
521
- if (savedSession?.surfaceModes) {
522
- this.surfaceModes = Object.fromEntries(
523
- Object.entries(savedSession.surfaceModes || {}).map(([surfaceId, mode]) => [
524
- surfaceId,
525
- normalizeSurfaceMode(mode),
526
- ]),
527
- );
528
- }
529
- if (Number.isFinite(Number(savedSession?.width))) this.width = Number(savedSession.width);
530
- this._restoreOpenRequested = Boolean(savedSession?.isOpen && savedSession?.activeSurfaceId);
531
- } catch (error) {
532
- console.warn("Could not restore right canvas session state", error);
533
- sessionStorage.removeItem(SESSION_STORAGE_KEY);
534
- }
535
- } else {
536
- sessionStorage.removeItem(SESSION_STORAGE_KEY);
537
- }
538
-
539
- this.setWidth(this.width, { persist: false });
540
- },
541
-
542
- updateLayoutMode() {
543
- const width = viewportWidth();
544
- const wasMobileMode = this.isMobileMode;
545
- this.isOverlayMode = width < DESKTOP_BREAKPOINT;
546
- this.isMobileMode = width <= MOBILE_BREAKPOINT;
547
- if (this.isMobileMode) {
548
- const wasOpen = this.isOpen;
549
- const mountedIds = this.mountedSurfaceIds();
550
- this.isOpen = false;
551
- this.mountedSurfaces = {};
552
- if ((wasOpen || mountedIds.length > 0) && mountedIds.length > 0) {
553
- globalThis.setTimeout?.(() => {
554
- for (const surfaceId of mountedIds) {
555
- const surface = this.getSurface(surfaceId);
556
- const payload = this._lastPayloadBySurface[surfaceId] || {};
557
- surface?.close?.({ ...payload, reason: "mobile" });
558
- }
559
- }, 0);
560
- }
561
- } else if (wasMobileMode && this.width < MIN_WIDTH) {
562
- this.width = this.defaultWidth();
563
- }
564
- },
565
-
566
- applyLayoutState() {
567
- this.updateLayoutMode();
568
- document.documentElement.style.setProperty("--right-canvas-width", `${this.width}px`);
569
- document.body.classList.toggle("right-canvas-open", this.isOpen && !this.isMobileMode && this.shouldRender());
570
- document.body.classList.toggle("right-canvas-overlay-mode", this.isOverlayMode);
571
- document.body.classList.toggle("right-canvas-mobile-mode", this.isMobileMode);
572
- this.scheduleRestoreOpenSurface();
573
- },
574
-
575
- widthStyle() {
576
- if (this.isMobileMode) return "";
577
- if (!this.isOpen) return "width: 0;";
578
- if (this.isOverlayMode) {
579
- return `width: min(${this.width}px, calc(100vw - 44px));`;
580
- }
581
- return `width: ${this.width}px;`;
582
- },
583
-
584
- getSurface(id) {
585
- const targetId = normalizeSurfaceId(id);
586
- return this.surfaces.find((surface) => surface.id === targetId)
587
- || getRegisteredSurfaces().find((surface) => surface.id === targetId)
588
- || null;
589
- },
590
-
591
- get railSurfaces() {
592
- return this.surfaces;
593
- },
594
-
595
- get panelSurfaces() {
596
- return this.surfaces.filter((surface) => !surface.actionOnly);
597
- },
598
-
599
- currentSurface() {
600
- return this.getSurface(this.activeSurfaceId);
601
- },
602
-
603
- isSurfaceActive(id) {
604
- return this.activeSurfaceId === normalizeSurfaceId(id);
605
- },
606
-
607
- activeTitle() {
608
- return this.currentSurface()?.title || "Canvas";
609
- },
610
-
611
- isWelcomeVisible() {
612
- return !chatsStore.selected;
613
- },
614
-
615
- shouldRender() {
616
- return !this.isWelcomeVisible();
617
- },
618
- };
619
-
620
- export const store = createStore("rightCanvas", model);
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:594000d14b05173a185721add68a5bfc02e6b71de6ec5d72b1e915190f826931
3
+ size 19660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/canvas/right-canvas.css CHANGED
@@ -1,384 +1,3 @@
1
- .container > *,
2
- #right-panel {
3
- min-width: 0;
4
- }
5
-
6
- #right-panel {
7
- position: relative;
8
- }
9
-
10
- .container > x-component[path="canvas/right-canvas.html"],
11
- .container > x-component[path="canvas/right-canvas.html"] > div[x-data] {
12
- display: flex;
13
- flex: 0 0 auto;
14
- height: 100%;
15
- min-width: 0;
16
- min-height: 0;
17
- }
18
-
19
- body.right-canvas-resizing {
20
- cursor: col-resize;
21
- user-select: none;
22
- }
23
-
24
- .right-canvas {
25
- --right-canvas-chrome: color-mix(in srgb, var(--color-background) 94%, #000 6%);
26
- --right-canvas-surface: color-mix(in srgb, var(--color-panel) 88%, var(--color-background) 12%);
27
- --right-canvas-border: color-mix(in srgb, var(--color-border) 70%, transparent);
28
- position: relative;
29
- z-index: 1200;
30
- display: flex;
31
- flex: 0 0 auto;
32
- height: 100%;
33
- min-width: 0;
34
- max-width: none;
35
- overflow: hidden;
36
- border-left: 1px solid var(--right-canvas-border);
37
- background: var(--right-canvas-chrome);
38
- transition: width 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
39
- }
40
-
41
- .right-canvas-rail {
42
- position: absolute;
43
- top: 33%;
44
- right: 100%;
45
- z-index: 4;
46
- display: flex;
47
- flex: 0 0 52px;
48
- flex-direction: column;
49
- align-items: center;
50
- gap: 7px;
51
- height: auto;
52
- max-height: min(70vh, calc(100vh - 32px));
53
- padding: 10px 7px;
54
- transform: translateY(-50%);
55
- border-right: 0;
56
- border-radius: 8px 0 0 8px;
57
- box-shadow: -10px 0 30px rgba(0, 0, 0, 0.24);
58
- background: var(--color-chat-background);
59
- }
60
-
61
- .right-canvas-rail-separator {
62
- width: 24px;
63
- height: 1px;
64
- margin: 1px 0 3px;
65
- background: color-mix(in srgb, var(--right-canvas-border) 76%, transparent);
66
- }
67
-
68
- .right-canvas.is-closed {
69
- width: 0 !important;
70
- min-width: 0;
71
- overflow: visible;
72
- border-left: 0;
73
- background: transparent;
74
- }
75
-
76
- .right-canvas.is-open {
77
- overflow: visible;
78
- }
79
-
80
- .right-canvas.is-closed .right-canvas-shell,
81
- .right-canvas.is-closed .right-canvas-resize-handle {
82
- display: none;
83
- }
84
-
85
- .right-canvas-rail-button,
86
- .right-canvas-icon-button,
87
- .right-canvas-tab {
88
- appearance: none;
89
- border: 1px solid transparent;
90
- color: var(--color-text);
91
- font: inherit;
92
- cursor: pointer;
93
- }
94
-
95
- .right-canvas-rail-button,
96
- .right-canvas-icon-button {
97
- display: inline-flex;
98
- align-items: center;
99
- justify-content: center;
100
- width: 34px;
101
- height: 34px;
102
- min-width: 34px;
103
- min-height: 34px;
104
- padding: 0;
105
- border-radius: 7px;
106
- background: transparent;
107
- opacity: 0.76;
108
- transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
109
- }
110
-
111
- .right-canvas-toggle-button {
112
- color: color-mix(in srgb, var(--color-primary) 70%, var(--color-text));
113
- }
114
-
115
- .right-canvas-rail-button:hover,
116
- .right-canvas-icon-button:hover,
117
- .right-canvas-rail-button.is-active {
118
- opacity: 1;
119
- border-color: color-mix(in srgb, var(--color-primary) 28%, var(--right-canvas-border));
120
- background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
121
- }
122
-
123
- .right-canvas-rail-button .material-symbols-outlined,
124
- .right-canvas-icon-button .material-symbols-outlined {
125
- font-size: 19px;
126
- }
127
-
128
- .right-canvas-surface-image {
129
- display: block;
130
- width: 22px;
131
- height: 22px;
132
- border-radius: 6px;
133
- object-fit: cover;
134
- }
135
-
136
- .right-canvas-shell {
137
- display: flex;
138
- flex: 1 1 auto;
139
- min-width: 0;
140
- min-height: 0;
141
- flex-direction: column;
142
- }
143
-
144
- .right-canvas-header {
145
- position: relative;
146
- z-index: 10;
147
- display: grid;
148
- grid-template-columns: minmax(0, 1fr) auto;
149
- align-items: center;
150
- gap: 8px;
151
- min-height: 45px;
152
- padding: 7px 8px 0 10px;
153
- border-bottom: 1px solid var(--right-canvas-border);
154
- background: color-mix(in srgb, var(--color-background) 91%, #000 9%);
155
- overflow: visible;
156
- }
157
-
158
- .right-canvas-tabs {
159
- display: flex;
160
- align-items: end;
161
- gap: 4px;
162
- min-width: 0;
163
- overflow-x: auto;
164
- overflow-y: hidden;
165
- scrollbar-width: thin;
166
- }
167
-
168
- .right-canvas-tabs::-webkit-scrollbar {
169
- height: 4px;
170
- }
171
-
172
- .right-canvas-tabs::-webkit-scrollbar-track {
173
- background: transparent;
174
- }
175
-
176
- .right-canvas-tabs::-webkit-scrollbar-thumb {
177
- background: color-mix(in srgb, var(--color-border) 76%, transparent);
178
- border-radius: 999px;
179
- }
180
-
181
- .right-canvas-tab {
182
- display: inline-flex;
183
- flex: 0 1 auto;
184
- align-items: center;
185
- gap: 7px;
186
- min-width: 0;
187
- padding: 0 10px;
188
- background: transparent;
189
- opacity: 0.4;
190
- white-space: nowrap;
191
- transition: border-color 0.16s ease, background-color 0.16s ease, opacity 0.16s ease;
192
- }
193
-
194
- .right-canvas-tab:hover,
195
- .right-canvas-tab.is-active {
196
- opacity: 1;
197
- }
198
-
199
- .right-canvas-tab .material-symbols-outlined {
200
- flex: 0 0 auto;
201
- font-size: 18px;
202
- }
203
-
204
- .right-canvas-tab .right-canvas-surface-image {
205
- flex: 0 0 auto;
206
- width: 18px;
207
- height: 18px;
208
- border-radius: 5px;
209
- }
210
-
211
- .right-canvas-tab-label {
212
- min-width: 0;
213
- overflow: hidden;
214
- text-overflow: ellipsis;
215
- font-size: 0.84rem;
216
- font-weight: 650;
217
- }
218
-
219
- .right-canvas-toolbar {
220
- position: relative;
221
- z-index: 11;
222
- display: flex;
223
- align-items: center;
224
- gap: 5px;
225
- padding-bottom: 6px;
226
- overflow: visible;
227
- }
228
-
229
- .right-canvas-panels {
230
- position: relative;
231
- z-index: 1;
232
- display: flex;
233
- flex: 1 1 auto;
234
- min-width: 0;
235
- min-height: 0;
236
- overflow: hidden;
237
- background: var(--right-canvas-surface);
238
- }
239
-
240
- .right-canvas-panels > x-extension {
241
- display: contents;
242
- }
243
-
244
- .right-canvas-panels > x-extension > x-component {
245
- display: contents;
246
- }
247
-
248
- .right-canvas-surface-panel {
249
- position: absolute;
250
- inset: 0;
251
- display: flex;
252
- flex: 0 0 auto;
253
- width: 100%;
254
- height: 100%;
255
- min-width: 0;
256
- min-height: 0;
257
- overflow: hidden;
258
- opacity: 0;
259
- pointer-events: none;
260
- z-index: 0;
261
- }
262
-
263
- .right-canvas-surface-panel.is-active {
264
- opacity: 1;
265
- pointer-events: auto;
266
- z-index: 2;
267
- }
268
-
269
- .right-canvas-surface-panel > x-component,
270
- .right-canvas-surface-panel > x-component > div[x-data] {
271
- display: flex;
272
- flex: 1 1 auto;
273
- width: 100%;
274
- height: 100%;
275
- min-width: 0;
276
- min-height: 0;
277
- }
278
-
279
- .right-canvas-empty-state {
280
- display: grid;
281
- flex: 1 1 auto;
282
- place-items: center;
283
- align-content: center;
284
- gap: 8px;
285
- color: color-mix(in srgb, var(--color-text) 60%, transparent);
286
- }
287
-
288
- .right-canvas-empty-state .material-symbols-outlined {
289
- font-size: 28px;
290
- }
291
-
292
- .right-canvas-resize-handle {
293
- position: absolute;
294
- top: 0;
295
- bottom: 0;
296
- left: -3px;
297
- z-index: 3;
298
- width: 7px;
299
- cursor: col-resize;
300
- }
301
-
302
- body.right-canvas-overlay-mode .right-canvas {
303
- position: fixed;
304
- top: 0;
305
- right: 0;
306
- bottom: 0;
307
- height: 100%;
308
- max-width: calc(100vw - 44px);
309
- box-shadow: -16px 0 38px rgba(0, 0, 0, 0.34);
310
- transform: translateX(100%);
311
- }
312
-
313
- body.right-canvas-overlay-mode .right-canvas.is-open {
314
- transform: translateX(0);
315
- }
316
-
317
- body.right-canvas-overlay-mode .right-canvas-resize-handle {
318
- display: none;
319
- }
320
-
321
- body.right-canvas-mobile-mode .right-canvas {
322
- position: fixed;
323
- top: 0;
324
- right: 0;
325
- bottom: 0;
326
- z-index: 3400;
327
- display: flex !important;
328
- width: 0 !important;
329
- min-width: 0;
330
- height: 100%;
331
- overflow: visible;
332
- transform: none;
333
- border-left: 0;
334
- background: transparent;
335
- box-shadow: none;
336
- pointer-events: none;
337
- }
338
-
339
- body.right-canvas-mobile-mode .right-canvas.is-open {
340
- transform: none;
341
- }
342
-
343
- body.right-canvas-mobile-mode .right-canvas-rail {
344
- position: fixed;
345
- top: 50%;
346
- right: max(0px, env(safe-area-inset-right));
347
- display: flex;
348
- transform: translateY(-50%);
349
- pointer-events: auto;
350
- }
351
-
352
- body.right-canvas-mobile-mode .right-canvas-shell,
353
- body.right-canvas-mobile-mode .right-canvas-resize-handle {
354
- display: none !important;
355
- }
356
-
357
- @media (max-width: 420px) {
358
- body.right-canvas-mobile-mode .right-canvas-rail {
359
- gap: 5px;
360
- padding: 8px 6px;
361
- transform: translateY(-50%) scale(0.92);
362
- transform-origin: right center;
363
- box-shadow: -8px 0 24px rgba(0, 0, 0, 0.22);
364
- }
365
-
366
- body.right-canvas-mobile-mode .right-canvas-rail-button,
367
- body.right-canvas-mobile-mode .right-canvas-icon-button {
368
- width: 32px;
369
- height: 32px;
370
- min-width: 32px;
371
- min-height: 32px;
372
- }
373
-
374
- body.right-canvas-mobile-mode .right-canvas-rail-button .material-symbols-outlined,
375
- body.right-canvas-mobile-mode .right-canvas-icon-button .material-symbols-outlined {
376
- font-size: 18px;
377
- }
378
- }
379
-
380
- @media (max-width: 480px) {
381
- .right-canvas-tab-label {
382
- display: none;
383
- }
384
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f2075f93cfeb6151b7bf50eaca76aed323ba3fa8a0fc24c491e8f12da19fc405
3
+ size 8046
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/canvas/right-canvas.html CHANGED
@@ -1,132 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store } from "/components/canvas/right-canvas-store.js";
5
- import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
6
- </script>
7
- </head>
8
- <body>
9
- <div x-data>
10
- <template x-if="$store.rightCanvas">
11
- <aside
12
- id="right-canvas"
13
- class="right-canvas"
14
- :class="{
15
- 'is-open': $store.rightCanvas.isOpen,
16
- 'is-closed': !$store.rightCanvas.isOpen,
17
- 'is-overlay': $store.rightCanvas.isOverlayMode,
18
- 'is-mobile': $store.rightCanvas.isMobileMode
19
- }"
20
- :style="$store.rightCanvas.widthStyle()"
21
- x-show="$store.rightCanvas.shouldRender()"
22
- x-init="$store.rightCanvas.init($el)"
23
- x-effect="$store.rightCanvas.applyLayoutState()"
24
- aria-label="Universal Canvas"
25
- >
26
- <div class="right-canvas-resize-handle" title="Resize canvas" @pointerdown="$store.rightCanvas.startResize($event)"></div>
27
-
28
- <nav class="right-canvas-rail" aria-label="Canvas surfaces" x-show="$store.preferences.isUiControlVisible('rightCanvasRail')">
29
- <button
30
- type="button"
31
- class="right-canvas-rail-button right-canvas-toggle-button"
32
- :class="{ 'is-active': $store.rightCanvas.isOpen }"
33
- :title="$store.rightCanvas.isOpen ? 'Hide canvas' : 'Show canvas'"
34
- :aria-label="$store.rightCanvas.isOpen ? 'Hide canvas' : 'Show canvas'"
35
- :aria-pressed="$store.rightCanvas.isOpen.toString()"
36
- @click="$store.rightCanvas.toggleCanvas()"
37
- >
38
- <x-icon name="dock_to_right"></x-icon>
39
- </button>
40
-
41
- <div class="right-canvas-rail-separator" aria-hidden="true"></div>
42
-
43
- <template x-for="surface in $store.rightCanvas.railSurfaces" :key="surface.id">
44
- <button
45
- type="button"
46
- class="right-canvas-rail-button"
47
- :class="{ 'is-active': $store.rightCanvas.isSurfaceActive(surface.id) && $store.rightCanvas.isOpen }"
48
- :title="surface.title"
49
- :aria-label="surface.title"
50
- @click="$store.rightCanvas.open(surface.id)"
51
- >
52
- <template x-if="surface.image">
53
- <img class="right-canvas-surface-image" :src="surface.image" alt="" aria-hidden="true">
54
- </template>
55
- <template x-if="!surface.image">
56
- <x-icon :name="surface.icon"></x-icon>
57
- </template>
58
- </button>
59
- </template>
60
- </nav>
61
-
62
- <section class="right-canvas-shell">
63
- <x-extension id="right-canvas-shell-start"></x-extension>
64
-
65
- <header class="right-canvas-header">
66
- <div class="right-canvas-tabs" role="tablist" aria-label="Canvas tabs">
67
- <x-extension id="right-canvas-tabs-start"></x-extension>
68
- <template x-for="surface in $store.rightCanvas.panelSurfaces" :key="surface.id">
69
- <button
70
- type="button"
71
- class="right-canvas-tab"
72
- role="tab"
73
- :aria-selected="$store.rightCanvas.isSurfaceActive(surface.id).toString()"
74
- :title="surface.title"
75
- :class="{ 'is-active': $store.rightCanvas.isSurfaceActive(surface.id) }"
76
- @click="$store.rightCanvas.open(surface.id)"
77
- >
78
- <template x-if="surface.image">
79
- <img class="right-canvas-surface-image" :src="surface.image" alt="" aria-hidden="true">
80
- </template>
81
- <template x-if="!surface.image">
82
- <x-icon aria-hidden="true" :name="surface.icon"></x-icon>
83
- </template>
84
- <span class="right-canvas-tab-label" x-text="surface.title"></span>
85
- </button>
86
- </template>
87
- <x-extension id="right-canvas-tabs-end"></x-extension>
88
- </div>
89
-
90
- <div class="right-canvas-toolbar" aria-label="Canvas toolbar">
91
- <x-extension id="right-canvas-toolbar-start"></x-extension>
92
- <button
93
- type="button"
94
- class="right-canvas-icon-button"
95
- title="Open as window"
96
- aria-label="Open as window"
97
- x-show="$store.rightCanvas.currentSurfaceCanUndock()"
98
- @click="$store.rightCanvas.undockActiveSurface()"
99
- >
100
- <x-icon name="open_in_new"></x-icon>
101
- </button>
102
- <button
103
- type="button"
104
- class="right-canvas-icon-button right-canvas-close-button"
105
- title="Close canvas"
106
- aria-label="Close canvas"
107
- @click="$store.rightCanvas.close()"
108
- >
109
- <x-icon name="close"></x-icon>
110
- </button>
111
- <x-extension id="right-canvas-toolbar-end"></x-extension>
112
- </div>
113
- </header>
114
-
115
- <main class="right-canvas-panels">
116
- <x-extension id="right-canvas-panels"></x-extension>
117
- <template x-if="$store.rightCanvas.panelSurfaces.length === 0">
118
- <div class="right-canvas-empty-state">
119
- <x-extension id="right-canvas-empty-state"></x-extension>
120
- <x-icon name="space_dashboard"></x-icon>
121
- <span>Canvas</span>
122
- </div>
123
- </template>
124
- </main>
125
-
126
- <x-extension id="right-canvas-shell-end"></x-extension>
127
- </section>
128
- </aside>
129
- </template>
130
- </div>
131
- </body>
132
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:325a3338a45d7df92b147f236725c639834a12c4ab1ee0ae603c4ede9e8dd624
3
+ size 5617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/AGENTS.md CHANGED
@@ -1,47 +1,3 @@
1
- # Chat Components DOX
2
-
3
- ## Purpose
4
-
5
- - Own chat composer, attachments, message queue, navigation, and top-section component groups.
6
-
7
- ## Ownership
8
-
9
- - `input/` owns composer input, progress, banners, and bottom action bars.
10
- - `attachments/` owns drag/drop and attachment preview UI.
11
- - `message-queue/` owns queued message display and store state.
12
- - `navigation/` owns chat navigation state.
13
- - `top-section/` owns chat header/top area.
14
- - `model-gate-store.js` and `model-setup-gate.html` own the deferred in-thread model setup gate.
15
-
16
- ## Local Contracts
17
-
18
- - Preserve Store Gating for all store-backed chat components.
19
- - Use shared API, WebSocket, notification, and attachment helpers where available.
20
- - Do not bypass CSRF or WebSocket state-sync expectations.
21
- - The shared composer can be mounted on the Welcome screen with no selected chat; sending from that state must create and select a chat context before dispatch.
22
- - Text-only and attachment sends must render the first user turn immediately with the request message ID so the backend log merges into the same row.
23
- - Unsent composer text is kept as a separate browser-session draft for each selected chat and restored when switching contexts; a Welcome-screen prompt must follow the chat created for its first send.
24
- - Composer text uses the main UI font by default; typing a triple-backtick fence and pressing Enter turns that line into a visual code block that serializes back to fenced Markdown, while pasted fenced Markdown stays plain text.
25
- - Missing model setup is gated at send intent: the first unconfigured send renders an in-thread setup card, keeps the pending prompt in browser session storage for refresh recovery, and must not call `/message_async` until a chat model is configured.
26
- - While the setup gate is open, the composer remains typeable but send is blocked until setup succeeds.
27
- - The setup gate must delegate Cloud/Local setup, account connections, and advanced model configuration to the existing onboarding and model-preset editor modals; do not duplicate provider/model/key forms inline.
28
- - A connected OAuth account without Main/Utility model selection is its own gate state; route to model configuration and do not select models automatically.
29
- - Model setup surfaces that change readiness must notify the gate with `model-setup-changed`, `model-configured`, or an existing modal/onboarding completion signal so the pending prompt can retry automatically.
30
- - The top-section project selector, clock, and connection indicator must respect the instance-level mobile/desktop visibility preferences.
31
- - While the selected context is running, an empty composer makes the primary button stop the active run; typed text still adds to the queue, and Enter with an empty composer still sends queued messages.
32
- - Chat navigation controls must cross virtual message-window boundaries; top and bottom target the full cached history rather than only the mounted DOM slice.
33
- - The page-wide attachment drop overlay must activate only for external file drags so internal WebUI drag interactions keep their own targets.
34
-
35
- ## Work Guidance
36
-
37
- - Keep composer and attachment changes responsive across desktop and mobile.
38
- - Coordinate payload changes with backend chat, upload, and WebSocket handlers.
39
-
40
- ## Verification
41
-
42
- - Smoke-test sending, queued messages, attachments, drag/drop, and navigation after visible changes.
43
- - Smoke-test the unconfigured first-send gate and automatic dispatch after model setup when touching model setup or send interception.
44
-
45
- ## Child DOX Index
46
-
47
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:145d134096c801ba7884cdf7ca6a494950af6f4f9afbb704ac5f97e7d7eac5ac
3
+ size 3545
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/attachments/attachmentsStore.js CHANGED
@@ -1,511 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import { fetchApi } from "/js/api.js";
3
- import { store as imageViewerStore } from "../../modals/image-viewer/image-viewer-store.js";
4
-
5
- const model = {
6
- // State properties
7
- attachments: [],
8
- hasAttachments: false,
9
- dragDropOverlayVisible: false,
10
-
11
- async init() {
12
- await this.initialize();
13
- },
14
-
15
- // Initialize the store
16
- async initialize() {
17
- // Setup event listeners for drag and drop
18
- this.setupDragDropHandlers();
19
- // Setup paste event listener for clipboard images
20
- this.setupPasteHandler();
21
- },
22
-
23
- // Basic attachment management methods
24
- addAttachment(attachment) {
25
- // Validate for duplicates
26
- if (this.validateDuplicates(attachment)) {
27
- this.attachments.push(attachment);
28
- this.updateAttachmentState();
29
- }
30
- },
31
-
32
- removeAttachment(index) {
33
- if (index >= 0 && index < this.attachments.length) {
34
- this.attachments.splice(index, 1);
35
- this.updateAttachmentState();
36
- }
37
- },
38
-
39
- clearAttachments() {
40
- this.attachments = [];
41
- this.updateAttachmentState();
42
- },
43
-
44
- validateDuplicates(newAttachment) {
45
- // Check if attachment already exists based on name and size
46
- const isDuplicate = this.attachments.some(
47
- (existing) =>
48
- existing.name === newAttachment.name &&
49
- existing.file &&
50
- newAttachment.file &&
51
- existing.file.size === newAttachment.file.size
52
- );
53
- return !isDuplicate;
54
- },
55
-
56
- updateAttachmentState() {
57
- this.hasAttachments = this.attachments.length > 0;
58
- },
59
-
60
- // Drag drop overlay control methods
61
- showDragDropOverlay() {
62
- this.dragDropOverlayVisible = true;
63
- },
64
-
65
- hideDragDropOverlay() {
66
- this.dragDropOverlayVisible = false;
67
- },
68
-
69
- isExternalFileDrag(event) {
70
- return Array.from(event?.dataTransfer?.types || []).includes("Files");
71
- },
72
-
73
- // Setup drag and drop event handlers
74
- setupDragDropHandlers() {
75
- console.log("Setting up drag and drop handlers...");
76
- let dragCounter = 0;
77
-
78
- // Prevent default drag behaviors
79
- ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
80
- document.addEventListener(
81
- eventName,
82
- (e) => {
83
- if (!this.isExternalFileDrag(e)) return;
84
- e.preventDefault();
85
- e.stopPropagation();
86
- },
87
- false
88
- );
89
- });
90
-
91
- // Handle drag enter
92
- document.addEventListener(
93
- "dragenter",
94
- (e) => {
95
- if (!this.isExternalFileDrag(e)) return;
96
- console.log("Drag enter detected");
97
- dragCounter++;
98
- if (dragCounter === 1) {
99
- console.log("Showing drag drop overlay");
100
- this.showDragDropOverlay();
101
- }
102
- },
103
- false
104
- );
105
-
106
- // Handle drag leave
107
- document.addEventListener(
108
- "dragleave",
109
- (e) => {
110
- if (!this.isExternalFileDrag(e) && dragCounter === 0) return;
111
- dragCounter--;
112
- if (dragCounter === 0) {
113
- this.hideDragDropOverlay();
114
- }
115
- },
116
- false
117
- );
118
-
119
- // Handle drop
120
- document.addEventListener(
121
- "drop",
122
- async (e) => {
123
- if (!this.isExternalFileDrag(e)) return;
124
- const dataTransfer = e.dataTransfer;
125
- console.log("Drop detected with files:", dataTransfer?.files?.length || 0);
126
- dragCounter = 0;
127
- this.hideDragDropOverlay();
128
-
129
- let files = [];
130
- try {
131
- files = await this.getDroppedFiles(dataTransfer);
132
- } catch (error) {
133
- console.error("Failed to read dropped files:", error);
134
- files = Array.from(dataTransfer?.files || []);
135
- }
136
- this.handleFiles(files);
137
- },
138
- false
139
- );
140
- },
141
-
142
- // Setup paste event handler for clipboard images
143
- setupPasteHandler() {
144
- console.log("Setting up paste handler...");
145
- document.addEventListener("paste", (e) => {
146
- // console.log("Paste event detected, target:", e.target.id);
147
- if (
148
- e.target.id !== "chat-input" &&
149
- e.target.id !== "full-screen-input"
150
- ) return;
151
-
152
- const items = e.clipboardData.items;
153
- let imageFound = false;
154
- console.log("Checking clipboard items:", items.length);
155
-
156
- // First, check if there are any images in the clipboard
157
- for (let i = 0; i < items.length; i++) {
158
- const item = items[i];
159
- if (item.type.indexOf("image") !== -1) {
160
- imageFound = true;
161
- const blob = item.getAsFile();
162
- if (blob) {
163
- e.preventDefault(); // Prevent default paste behavior for images
164
- this.handleClipboardImage(blob);
165
- console.log("Image detected in clipboard, processing...");
166
- }
167
- break; // Only handle the first image found
168
- }
169
- }
170
-
171
- // If no images found and we're in an input field, let normal text paste happen
172
- if (
173
- !imageFound &&
174
- (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
175
- ) {
176
- console.log(
177
- "No images in clipboard, allowing normal text paste in input field"
178
- );
179
- return;
180
- }
181
-
182
- // If no images found and not in input field, do nothing
183
- if (!imageFound) {
184
- console.log("No images in clipboard");
185
- }
186
- });
187
- },
188
-
189
- // Handle clipboard image pasting
190
- async handleClipboardImage(blob) {
191
- try {
192
- // Generate unique filename
193
- const guid = this.generateGUID();
194
- const filename = `clipboard-${guid}.png`;
195
-
196
- // Create file object from blob
197
- const file = new File([blob], filename, { type: "image/png" });
198
-
199
- // Create attachment object
200
- const attachment = {
201
- file: file,
202
- type: "image",
203
- name: filename,
204
- extension: "png",
205
- displayInfo: this.getAttachmentDisplayInfo(file),
206
- };
207
-
208
- // Read as data URL for preview
209
- const reader = new FileReader();
210
- reader.onload = (e) => {
211
- attachment.url = e.target.result;
212
- this.addAttachment(attachment);
213
- };
214
- reader.readAsDataURL(file);
215
-
216
- // Show success feedback
217
- console.log("Clipboard image pasted successfully:", filename);
218
- } catch (error) {
219
- console.error("Failed to handle clipboard image:", error);
220
- }
221
- },
222
-
223
- // Update handleFileUpload to use the attachments store
224
- handleFileUpload(event) {
225
- const files = event.target.files;
226
- this.handleFiles(files);
227
- event.target.value = ""; // clear uploader selection to fix issue where same file is ignored the second time
228
- },
229
-
230
- async getDroppedFiles(dataTransfer) {
231
- const items = Array.from(dataTransfer?.items || []);
232
- const entries = items
233
- .map((item) =>
234
- typeof item.webkitGetAsEntry === "function"
235
- ? item.webkitGetAsEntry()
236
- : null
237
- )
238
- .filter(Boolean);
239
-
240
- if (!entries.length) {
241
- return Array.from(dataTransfer?.files || []);
242
- }
243
-
244
- const nested = await Promise.all(
245
- entries.map((entry) => this.readEntryFiles(entry))
246
- );
247
- const files = nested.flat();
248
- return files.length ? files : Array.from(dataTransfer?.files || []);
249
- },
250
-
251
- async readEntryFiles(entry) {
252
- if (entry.isFile) {
253
- return await new Promise((resolve, reject) => {
254
- entry.file(
255
- (file) => resolve([file]),
256
- (error) => reject(error)
257
- );
258
- });
259
- }
260
-
261
- if (!entry.isDirectory) return [];
262
-
263
- const reader = entry.createReader();
264
- const childEntries = await this.readAllDirectoryEntries(reader);
265
- const nested = await Promise.all(
266
- childEntries.map((child) => this.readEntryFiles(child))
267
- );
268
- return nested.flat();
269
- },
270
-
271
- async readAllDirectoryEntries(reader) {
272
- const entries = [];
273
-
274
- return await new Promise((resolve, reject) => {
275
- const readBatch = () => {
276
- reader.readEntries(
277
- (batch) => {
278
- if (!batch.length) {
279
- resolve(entries);
280
- return;
281
- }
282
- entries.push(...batch);
283
- readBatch();
284
- },
285
- (error) => reject(error)
286
- );
287
- };
288
-
289
- readBatch();
290
- });
291
- },
292
-
293
- // File handling logic (moved from index.js)
294
- handleFiles(files) {
295
- const fileList = Array.from(files || []);
296
- console.log("handleFiles called with", fileList.length, "files");
297
- fileList.forEach((file) => {
298
- if (!file?.name) return;
299
-
300
- console.log("Processing file:", file.name, file.type);
301
- const ext = file.name.split(".").pop().toLowerCase();
302
- const isImage = ["jpg", "jpeg", "png", "bmp", "gif", "webp", "svg"].includes(
303
- ext
304
- );
305
-
306
- const attachment = {
307
- file: file,
308
- type: isImage ? "image" : "file",
309
- name: file.name,
310
- extension: ext,
311
- displayInfo: this.getAttachmentDisplayInfo(file),
312
- };
313
-
314
- if (isImage) {
315
- // Read image as data URL for preview
316
- const reader = new FileReader();
317
- reader.onload = (e) => {
318
- attachment.url = e.target.result;
319
- this.addAttachment(attachment);
320
- };
321
- reader.readAsDataURL(file);
322
- } else {
323
- // For non-image files, add directly
324
- this.addAttachment(attachment);
325
- }
326
- });
327
- },
328
-
329
- // Get attachments for sending message
330
- getAttachmentsForSending() {
331
- return this.attachments.map((attachment) => {
332
- if (attachment.type === "image") {
333
- return {
334
- ...attachment,
335
- url: URL.createObjectURL(attachment.file),
336
- };
337
- } else {
338
- return {
339
- ...attachment,
340
- };
341
- }
342
- });
343
- },
344
-
345
- // Generate server-side API URL for file (for device sync)
346
- getServerImgUrl(filename) {
347
- return `/api/image_get?path=/a0/usr/uploads/${encodeURIComponent(filename)}`;
348
- },
349
-
350
- getServerFileUrl(filename) {
351
- return `/a0/usr/uploads/${encodeURIComponent(filename)}`;
352
- },
353
-
354
- // Check if file is an image based on extension
355
- isImageFile(filename) {
356
- const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
357
- const extension = filename.split(".").pop().toLowerCase();
358
- return imageExtensions.includes(extension);
359
- },
360
-
361
- // Get attachment preview URL (server URL for persistence, blob URL for current session)
362
- getAttachmentPreviewUrl(attachment) {
363
- // If attachment has a name and we're dealing with a server-stored file
364
- if (typeof attachment === "string") {
365
- // attachment is just a filename (from loaded chat)
366
- return this.getServerImgUrl(attachment);
367
- } else if (attachment.name && attachment.file) {
368
- // attachment is an object from current session
369
- if (attachment.type === "image") {
370
- // For images, use blob URL for current session preview
371
- return attachment.url || URL.createObjectURL(attachment.file);
372
- } else {
373
- // For non-image files, use server URL to get appropriate icon
374
- return this.getServerImgUrl(attachment.name);
375
- }
376
- }
377
- return null;
378
- },
379
-
380
- getFilePreviewUrl(filename) {
381
- const extension = filename.split(".").pop().toLowerCase();
382
- const types = {
383
- // Archive files
384
- zip: "archive",
385
- rar: "archive",
386
- "7z": "archive",
387
- tar: "archive",
388
- gz: "archive",
389
- // Document files
390
- pdf: "document",
391
- doc: "document",
392
- docx: "document",
393
- txt: "document",
394
- rtf: "document",
395
- odt: "document",
396
- // Code files
397
- py: "code",
398
- js: "code",
399
- html: "code",
400
- css: "code",
401
- json: "code",
402
- xml: "code",
403
- md: "code",
404
- yml: "code",
405
- yaml: "code",
406
- sql: "code",
407
- sh: "code",
408
- bat: "code",
409
- // Spreadsheet files
410
- xls: "document",
411
- xlsx: "document",
412
- csv: "document",
413
- // Presentation files
414
- ppt: "document",
415
- pptx: "document",
416
- odp: "document",
417
- };
418
- const type = types[extension] || "file";
419
- return `/public/${type}.svg`;
420
- },
421
-
422
- // Enhanced method to get attachment display info for UI
423
- getAttachmentDisplayInfo(attachment) {
424
- if (typeof attachment === "string") {
425
- // attachment is filename only (from persistent storage)
426
- const filename = attachment;
427
- const extension = filename.split(".").pop();
428
- const isImage = this.isImageFile(filename);
429
- const previewUrl = isImage
430
- ? this.getServerImgUrl(filename)
431
- : this.getFilePreviewUrl(filename);
432
-
433
- return {
434
- filename: filename,
435
- extension: extension.toUpperCase(),
436
- isImage: isImage,
437
- previewUrl: previewUrl,
438
- clickHandler: () => {
439
- if (this.isImageFile(filename)) {
440
- imageViewerStore.open(this.getServerImgUrl(filename), { name: filename });
441
- } else {
442
- this.downloadAttachment(filename);
443
- }
444
- },
445
- };
446
- } else {
447
- // attachment is object (from current session)
448
- const isImage = this.isImageFile(attachment.name);
449
- const filename = attachment.name;
450
- const extension = filename.split(".").pop() || "";
451
- const previewUrl = isImage
452
- ? this.getServerImgUrl(attachment.name)
453
- : this.getFilePreviewUrl(attachment.name);
454
- return {
455
- filename: filename,
456
- extension: extension.toUpperCase(),
457
- isImage: attachment.type === "image",
458
- previewUrl: previewUrl,
459
- clickHandler: () => {
460
- if (attachment.type === "image") {
461
- const imageUrl = this.getServerImgUrl(attachment.name);
462
- imageViewerStore.open(imageUrl, { name: attachment.name });
463
- } else {
464
- this.downloadAttachment(attachment.name);
465
- }
466
- },
467
- };
468
- }
469
- },
470
-
471
- async downloadAttachment(filename) {
472
- try {
473
- const path = this.getServerFileUrl(filename);
474
- const response = await fetchApi("/download_work_dir_file?path=" + path);
475
-
476
- if (!response.ok) {
477
- throw new Error("Network response was not ok");
478
- }
479
-
480
- const blob = await response.blob();
481
-
482
- const link = document.createElement("a");
483
- link.href = window.URL.createObjectURL(blob);
484
- link.download = filename;
485
- document.body.appendChild(link);
486
- link.click();
487
- document.body.removeChild(link);
488
- window.URL.revokeObjectURL(link.href);
489
- } catch (error) {
490
- window.toastFetchError("Error downloading file", error);
491
- alert("Error downloading file");
492
- }
493
- },
494
-
495
- // Generate GUID for unique filenames
496
- generateGUID() {
497
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
498
- /[xy]/g,
499
- function (c) {
500
- const r = (Math.random() * 16) | 0;
501
- const v = c == "x" ? r : (r & 0x3) | 0x8;
502
- return v.toString(16);
503
- }
504
- );
505
- },
506
-
507
- };
508
-
509
- const store = createStore("chatAttachments", model);
510
-
511
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:58ee9c5c4f3edd414df6a73c646170e88069cd365820df9f8de13646bbdcbf74
3
+ size 14540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/attachments/dragDropOverlay.html CHANGED
@@ -1,28 +1,3 @@
1
- <html>
2
-
3
- <head>
4
- <title>Drag Drop Overlay</title>
5
-
6
- <script type="module">
7
- import { store } from "/components/chat/attachments/attachmentsStore.js";
8
- </script>
9
- </head>
10
-
11
- <body>
12
- <!-- Drag and Drop Overlay -->
13
- <div x-data>
14
- <template x-if="$store.chatAttachments">
15
- <div x-cloak x-show="$store.chatAttachments.dragDropOverlayVisible" id="dragdrop-overlay"
16
- x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
17
- x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-300"
18
- x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="dragdrop-overlay">
19
- <img src="public/dragndrop.svg" alt="Drop files" class="dragdrop-icon">
20
- <div class="dragdrop-text">Drop files or folders to attach them to your message</div>
21
- <div class="dragdrop-subtext"></div>
22
- </div>
23
- </template>
24
- </div>
25
-
26
- </body>
27
-
28
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7171cdc28bf4c422adaef686aa8502f87871582b2eb4ab4e8f0a8495f29e6fb0
3
+ size 1034
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/attachments/inputPreview.html CHANGED
@@ -1,30 +1,3 @@
1
- <script type="module">
2
- import { store } from "/components/chat/attachments/attachmentsStore.js";
3
- import { store as imageViewerStore } from "/components/modals/image-viewer/image-viewer-store.js";
4
- </script>
5
-
6
- <div x-data>
7
- <template x-if="$store.chatAttachments">
8
-
9
- <div x-show="$store.chatAttachments.hasAttachments" class="preview-section scrollbar">
10
- <template x-for="(attachment, index) in $store.chatAttachments.attachments" :key="index">
11
- <div class="attachment-item"
12
- :class="{'image-type': attachment.type === 'image', 'file-type': attachment.type === 'file'}">
13
- <template x-if="attachment.type === 'image'">
14
- <img :src="attachment.url" class="attachment-preview" :alt="attachment.name" style="cursor: pointer;"
15
- @click="$store.imageViewer.open(attachment.url, { name: attachment.name })">
16
- </template>
17
- <template x-if="attachment.type === 'file'">
18
- <div>
19
- <img :src="attachment.displayInfo.previewUrl" class="file-icon" :alt="attachment.extension">
20
- <span class="file-title" x-text="attachment.name"></span>
21
- </div>
22
- </template>
23
- <button @click="$store.chatAttachments.removeAttachment(index)"
24
- class="remove-attachment">&times;</button>
25
- </div>
26
- </template>
27
- </div>
28
-
29
- </template>
30
- </div>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0255aaa44ae7179e3f8460a7e265f1671ee8f6d5ae362a658a402f90e8a0d00d
3
+ size 1561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/bottom-actions-bar.html CHANGED
@@ -1,98 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store } from "/components/chat/input/input-store.js";
5
- </script>
6
- </head>
7
- <body>
8
- <template x-if="$store.chatInput">
9
- <div class="text-buttons-row chat-bottom-actions-bar">
10
- <x-extension id="chat-input-bottom-actions-start"></x-extension>
11
- <button type="button" class="text-button" @click="$store.chatInput.pauseAgent(!$store.chatInput.paused)">
12
- <template x-if="!$store.chatInput.paused">
13
- <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14" aria-hidden="true">
14
- <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path>
15
- </svg>
16
- </template>
17
- <template x-if="$store.chatInput.paused">
18
- <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" width="14" height="14" aria-hidden="true">
19
- <path d="M8 5v14l11-7z"></path>
20
- </svg>
21
- </template>
22
- <span x-text="$store.chatInput.paused ? 'Resume Agent' : 'Pause Agent'"></span>
23
- </button>
24
-
25
- <button type="button" class="text-button" id="nudges_window" @click="$store.chatInput.nudge()">
26
- <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58" fill="currentColor" width="14" height="14" aria-hidden="true">
27
- <path d="m11.97,16.32c-.46,0-.91-.25-1.15-.68-.9-1.63-1.36-3.34-1.36-5.1C9.45,4.73,14.18,0,20,0s10.55,4.73,10.55,10.55c0,.87-.13,1.75-.41,2.76-.19.7-.9,1.13-1.62.93-.7-.19-1.12-.92-.93-1.62.21-.79.31-1.44.31-2.07,0-4.36-3.55-7.91-7.91-7.91s-7.91,3.55-7.91,7.91c0,1.3.35,2.59,1.03,3.82.36.64.13,1.44-.51,1.79-.21.11-.42.17-.64.17Z" stroke-width="0.5" stroke="currentColor"/>
28
- <path d="M34.5,58h-6.18c-3.17,0-6.15-1.23-8.39-3.47L1.16,35.75c-1.54-1.54-1.54-4.05,0-5.59,2.4-2.4,6.27-2.68,8.99-.64l4.58,3.44V10.55c0-2.91,2.36-5.27,5.27-5.27s5.27,2.36,5.27,5.27v8.62c.78-.45,1.68-.71,2.64-.71,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.91,0,5.27,2.36,5.27,5.27v14.5c0,8-6.51,14.5-14.5,14.5ZM6.03,30.79c-1.1,0-2.19.42-3.01,1.23-.51.51-.51,1.35,0,1.86l18.77,18.78c1.74,1.74,4.06,2.7,6.53,2.7h6.18c6.54,0,11.86-5.32,11.86-11.86v-14.5c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v1.32c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-3.95c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v3.95c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-6.59c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v6.59c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32V10.55c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v25.05c0,.5-.28.95-.73,1.18s-.98.18-1.38-.12l-6.69-5.02c-.75-.56-1.65-.84-2.54-.84Z" stroke-width="0.5" stroke="currentColor"/>
29
- </svg>
30
- <p>Nudge</p>
31
- </button>
32
- <x-extension id="chat-input-bottom-actions-end"></x-extension>
33
- </div>
34
- </template>
35
-
36
- <style>
37
- .text-buttons-row {
38
- width: 100%;
39
- display: flex;
40
- flex-wrap: wrap;
41
- gap: var(--spacing-xs);
42
- padding-top: var(--spacing-xs);
43
- margin-left: var(--spacing-xs);
44
- }
45
- .text-buttons-row > x-extension:not(:empty) { display: contents; }
46
- .text-buttons-row > x-extension:empty { display: none; }
47
- .text-buttons-row > x-extension > x-component,
48
- .text-buttons-row > x-extension > x-component > div[x-data] { display: contents; }
49
- .text-button {
50
- background-color: transparent;
51
- border: none;
52
- border-radius: 5px;
53
- color: var(--color-text);
54
- font-family: "Rubik", Arial, Helvetica, sans-serif;
55
- font-size: 0.6rem;
56
- padding: 6px var(--spacing-sm);
57
- cursor: pointer;
58
- opacity: 0.8;
59
- transition: all 0.3s;
60
- display: flex;
61
- align-items: center;
62
- gap: var(--spacing-xs);
63
- }
64
- .text-button:hover:not(:disabled) {
65
- opacity: 1;
66
- background-color: var(--color-background-hover);
67
- border-radius: 4px;
68
- }
69
- .text-button:active:not(:disabled) { opacity: 0.5; }
70
- .text-button:disabled {
71
- opacity: 0.4;
72
- cursor: not-allowed;
73
- }
74
- .text-button svg {
75
- width: 14px;
76
- height: 14px;
77
- flex-shrink: 0;
78
- }
79
- .text-button p { margin-block: 0; }
80
-
81
- @media (max-width: 768px) {
82
- .text-buttons-row {
83
- width: 90%;
84
- white-space: pre-wrap;
85
- }
86
- .text-button svg { width: 18px; height: 18px; }
87
- }
88
- @media (max-width: 640px) {
89
- .text-buttons-row {
90
- width: 90%;
91
- white-space: pre-wrap;
92
- }
93
- .text-button { max-height: 25px; }
94
- .text-button p { display: none; }
95
- }
96
- </style>
97
- </body>
98
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:50f132af7eee05b29dd84d7ab248b34e85c81ae9f79e43dc5a48694b4bc2cc6f
3
+ size 4662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/bottom-actions.html CHANGED
@@ -1,161 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store } from "/components/chat/input/input-store.js";
5
- import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6
- import { store as historyStore } from "/components/modals/history/history-store.js";
7
- import { store as contextStore } from "/components/modals/context/context-store.js";
8
- import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
9
- import { store as mcpServersStore } from "/components/settings/mcp/client/mcp-servers-store.js";
10
- </script>
11
- </head>
12
- <body>
13
- <template x-if="$store.chatInput">
14
- <div class="chat-bottom-actions-menu">
15
- <input
16
- type="file"
17
- x-ref="chatFileInput"
18
- class="chat-bottom-menu-file-input"
19
- accept="*"
20
- multiple
21
- @change="$store.chatAttachments.handleFileUpload($event); $store.chatInput.closeChatMoreMenu()"
22
- >
23
- <button
24
- type="button"
25
- class="chat-bottom-menu-item"
26
- @click.stop="$refs.chatFileInput?.click()"
27
- >
28
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
29
- <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
30
- </svg>
31
- <span>Attach files</span>
32
- </button>
33
-
34
- <input
35
- type="file"
36
- x-ref="chatFolderInput"
37
- class="chat-bottom-menu-file-input"
38
- webkitdirectory
39
- directory
40
- multiple
41
- @change="$store.chatAttachments.handleFileUpload($event); $store.chatInput.closeChatMoreMenu()"
42
- >
43
- <button
44
- type="button"
45
- class="chat-bottom-menu-item"
46
- @click.stop="$refs.chatFolderInput?.click()"
47
- >
48
- <x-icon aria-hidden="true" name="drive_folder_upload"></x-icon>
49
- <span>Attach folder</span>
50
- </button>
51
-
52
- <button
53
- type="button"
54
- class="chat-bottom-menu-item"
55
- @click="$store.mcpServersStore.openFromComposer($store.chats.selectedContext?.project?.name || ''); $store.chatInput.closeChatMoreMenu()"
56
- >
57
- <x-icon aria-hidden="true" name="hub"></x-icon>
58
- <span>MCP Servers</span>
59
- </button>
60
-
61
- <button
62
- type="button"
63
- class="chat-bottom-menu-item"
64
- @click="$confirmClick($event, () => { $store.chats.resetChat(); $store.chatInput.closeChatMoreMenu() })"
65
- :disabled="!$store.chats.selected"
66
- >
67
- <x-icon aria-hidden="true" name="delete_sweep"></x-icon>
68
- <span>Clear Chat</span>
69
- </button>
70
-
71
- <button
72
- type="button"
73
- class="chat-bottom-menu-item"
74
- id="history_inspect"
75
- @click="$store.history.open(); $store.chatInput.closeChatMoreMenu()"
76
- :disabled="!$store.chats.selected"
77
- >
78
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85" width="18" height="18" aria-hidden="true">
79
- <path fill="currentColor" d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.942-2.478-.946-3.42,0Z"></path>
80
- </svg>
81
- <span>History</span>
82
- </button>
83
-
84
- <button
85
- type="button"
86
- class="chat-bottom-menu-item"
87
- id="ctx_window"
88
- @click="$store.context.open(); $store.chatInput.closeChatMoreMenu()"
89
- :disabled="!$store.chats.selected"
90
- >
91
- <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" width="18" height="18" fill="currentColor" aria-hidden="true">
92
- <path d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
93
- <path d="m63 79c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
94
- <path d="m68 39h-36c-6.0703 0-11 4.9297-11 11s4.9297 11 11 11h36c6.0703 0 11-4.9297 11-11s-4.9297-11-11-11zm0 18h-36c-3.8594 0-7-3.1406-7-7s3.1406-7 7-7h36c3.8594 0 7 3.1406 7 7s-3.1406 7-7 7z"></path>
95
- </svg>
96
- <span>Context</span>
97
- </button>
98
- </div>
99
- </template>
100
-
101
- <style>
102
- .chat-bottom-actions-menu {
103
- display: flex;
104
- flex-direction: column;
105
- width: 100%;
106
- font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
107
- font-size: 0.8125rem;
108
- }
109
-
110
- .chat-bottom-menu-item {
111
- display: flex;
112
- align-items: center;
113
- gap: 0.4rem;
114
- width: 100%;
115
- margin: 0;
116
- padding: var(--spacing-xs) var(--spacing-sm);
117
- border-radius: 0.25rem;
118
- background: transparent;
119
- color: var(--color-text);
120
- font-family: 'Rubik', Arial, Helvetica, sans-serif;
121
- text-align: left;
122
- cursor: pointer;
123
- opacity: 0.88;
124
- transition: background-color 0.12s ease, opacity 0.12s ease;
125
- white-space: nowrap;
126
- box-sizing: border-box;
127
- }
128
- button.chat-bottom-menu-item {
129
- border: none;
130
- }
131
- .chat-bottom-menu-file-input {
132
- position: absolute;
133
- width: 1px;
134
- height: 1px;
135
- padding: 0;
136
- margin: -1px;
137
- overflow: hidden;
138
- clip: rect(0, 0, 0, 0);
139
- white-space: nowrap;
140
- border: 0;
141
- opacity: 0;
142
- }
143
- button.chat-bottom-menu-item:hover:not(:disabled) {
144
- opacity: 1;
145
- background-color: var(--color-background-hover);
146
- }
147
- button.chat-bottom-menu-item:active:not(:disabled) { opacity: 0.72; }
148
- button.chat-bottom-menu-item:disabled {
149
- opacity: 0.38;
150
- cursor: not-allowed;
151
- pointer-events: none;
152
- }
153
- .chat-bottom-menu-item .material-symbols-outlined {
154
- font-size: 18px;
155
- flex-shrink: 0;
156
- opacity: 0.9;
157
- }
158
- .chat-bottom-menu-item svg { flex-shrink: 0; opacity: 0.88; }
159
- </style>
160
- </body>
161
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:acb3a816b76c3bf32a318d6f297e7b0f57752b191e3af49b3e66702019044738
3
+ size 7639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/chat-bar-input.html CHANGED
@@ -1,457 +1,3 @@
1
- <html>
2
-
3
- <head>
4
- <script type="module">
5
- import { store as fullScreenStore } from "/components/modals/full-screen-input/full-screen-store.js";
6
- import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
7
- import { store as chatInputStore } from "/components/chat/input/input-store.js";
8
- </script>
9
- </head>
10
-
11
- <body>
12
- <div x-data>
13
- <x-extension id="chat-input-box-start"></x-extension>
14
- <div class="input-row">
15
- <!-- More actions (+): opens upward; content lives in bottom-actions.html -->
16
- <div class="attachment-wrapper chat-more-dropdown" @click.outside="$store.chatInput.closeChatMoreMenu()"
17
- @keydown.escape.window="$store.chatInput.closeChatMoreMenu()">
18
- <button type="button" class="chat-more-trigger" @click.stop="$store.chatInput.toggleChatMoreMenu()"
19
- :aria-expanded="$store.chatInput.chatMoreMenuOpen.toString()" aria-label="More actions">
20
- <x-icon aria-hidden="true" name="add"></x-icon>
21
- </button>
22
- <div class="chat-more-panel" x-show="$store.chatInput.chatMoreMenuOpen" x-transition style="display: none;">
23
- <x-component path="chat/input/bottom-actions.html"></x-component>
24
- </div>
25
- </div>
26
-
27
- <!-- Container for composer and expand button -->
28
- <div id="chat-input-container">
29
- <div id="chat-input" role="textbox" aria-label="Message" aria-multiline="true" contenteditable="true"
30
- spellcheck="true" :data-placeholder="$store.chatInput.inputPlaceholder"
31
- @keydown="$store.chatInput.handleKeydown($event)"
32
- @input="$store.chatInput.handleInput($event)"
33
- @paste="$store.chatInput.handlePaste($event)"
34
- :class="{ 'progress-active': $store.chatInput.progressActive, 'is-empty': !$store.chatInput.message }"
35
- x-create="$store.chatInput.mountEditor($el)"
36
- x-destroy="$store.chatInput.unmountEditor($el)"></div>
37
- <div id="chat-input-progress-placeholder" x-show="$store.chatInput.showProgressPlaceholder"
38
- x-html="$store.chatInput.progressPlaceholderHtml"
39
- :class="{ 'progress-active': $store.chatInput.progressActive }" aria-hidden="true" style="display: none;">
40
- </div>
41
- <button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
42
- <x-icon aria-hidden="true" name="fullscreen"></x-icon>
43
- </button>
44
- </div>
45
-
46
- <div id="chat-buttons-wrapper">
47
- <!-- Send button -->
48
- <button class="chat-button" id="send-button" @click="$store.chatInput.activateSendButton()"
49
- :class="$store.chatInput.sendButtonClass" :title="$store.chatInput.sendButtonTitle"
50
- :aria-label="$store.chatInput.sendButtonTitle">
51
- <x-icon :name="$store.chatInput.sendButtonIcon"></x-icon>
52
- </button>
53
- </div>
54
- </div>
55
-
56
- <!-- Full Screen Input Modal -->
57
- <x-component path="modals/full-screen-input/full-screen-input.html"></x-component>
58
- <x-extension id="chat-input-box-end"></x-extension>
59
- </div>
60
-
61
-
62
- <style>
63
- /* Layout rows */
64
- .input-row {
65
- display: flex;
66
- align-items: center;
67
- gap: 0.72rem;
68
- width: 100%;
69
- min-height: 4.25rem;
70
- padding: 0.42rem 0.52rem 0.42rem 0.72rem;
71
- border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
72
- border-radius: 8px;
73
- background: color-mix(in srgb, var(--color-panel) 72%, var(--color-background) 28%);
74
- box-shadow:
75
- inset 0 1px 0 rgba(255, 255, 255, 0.035),
76
- 0 18px 46px rgba(0, 0, 0, 0.18);
77
- white-space: nowrap;
78
- transition: border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
79
- }
80
-
81
- .input-row:focus-within {
82
- border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
83
- box-shadow:
84
- inset 0 1px 0 rgba(255, 255, 255, 0.045),
85
- 0 22px 54px rgba(0, 0, 0, 0.22);
86
- }
87
-
88
- .attachment-wrapper {
89
- position: relative;
90
- flex-shrink: 0;
91
- }
92
-
93
- /* Composer + menu: custom panel (edit here; not shared with file-browser dropdown) */
94
- .chat-more-dropdown {
95
- z-index: 1;
96
- }
97
-
98
- .chat-more-trigger {
99
- cursor: pointer;
100
- color: var(--color-text);
101
- opacity: 0.82;
102
- transition: opacity 0.16s ease, background-color 0.16s ease, color 0.16s ease;
103
- display: flex;
104
- align-items: center;
105
- justify-content: center;
106
- width: 2.5rem;
107
- height: 2.5rem;
108
- padding: 0;
109
- border: none;
110
- border-radius: 10px;
111
- background: transparent;
112
- }
113
-
114
- .chat-more-trigger:hover {
115
- opacity: 1;
116
- background: color-mix(in srgb, var(--color-border) 36%, transparent);
117
- }
118
-
119
- .chat-more-trigger:active {
120
- opacity: 0.5;
121
- }
122
-
123
- .chat-more-trigger .material-symbols-outlined {
124
- font-size: 1.7rem;
125
- line-height: 1;
126
- }
127
-
128
- .chat-more-panel {
129
- position: absolute;
130
- left: 0;
131
- bottom: 100%;
132
- margin-bottom: 0.25rem;
133
- min-width: 11rem;
134
- max-width: min(18rem, 92vw);
135
- max-height: min(70vh, 20rem);
136
- overflow-x: hidden;
137
- overflow-y: auto;
138
- background-color: color-mix(in srgb, var(--color-panel) 90%, black 10%);
139
- border: 1px solid var(--color-border);
140
- border-radius: 12px;
141
- box-shadow: 0 16px 38px rgba(0, 0, 0, 0.3);
142
- z-index: 1050;
143
- padding: 0.3rem;
144
- }
145
-
146
- .light-mode .chat-more-panel {
147
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
148
- }
149
-
150
- /* Text input */
151
- #chat-input-container {
152
- position: relative;
153
- width: 100%;
154
- min-width: 0;
155
- display: flex;
156
- align-items: center;
157
- align-self: stretch;
158
- }
159
-
160
- #chat-input {
161
- background-color: transparent;
162
- border: 0;
163
- border-radius: 0;
164
- color: var(--color-text);
165
- flex-grow: 1;
166
- font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
167
- font-optical-sizing: auto;
168
- -webkit-font-optical-sizing: auto;
169
- font-size: 1rem;
170
- line-height: 1.45;
171
- max-height: 30vh;
172
- min-height: 2.8rem;
173
- width: 100%;
174
- padding: 0.68rem 2.2rem 0.68rem 0.1rem;
175
- margin: 0;
176
- overflow-y: auto;
177
- white-space: pre-wrap;
178
- word-break: break-word;
179
- scroll-behavior: smooth;
180
- resize: none;
181
- align-content: center;
182
- background-clip: border-box;
183
- outline: none;
184
- transition: color 0.16s ease, background-color 0.16s ease;
185
- -webkit-transition: color 0.16s ease, background-color 0.16s ease;
186
- }
187
-
188
- #chat-input.is-empty::before {
189
- content: attr(data-placeholder);
190
- color: color-mix(in srgb, var(--color-text) 62%, transparent);
191
- opacity: 0.78;
192
- pointer-events: none;
193
- }
194
-
195
- #chat-input .composer-code-block {
196
- display: block;
197
- margin: 0.48rem 0;
198
- padding: 0.56rem 0.7rem;
199
- border-radius: 6px;
200
- background: color-mix(in srgb, var(--color-border) 28%, transparent);
201
- }
202
-
203
- #chat-input .composer-code-content {
204
- min-height: 1.35rem;
205
- margin: 0;
206
- outline: none;
207
- white-space: pre-wrap;
208
- word-break: break-word;
209
- font-family: var(--font-family-code, "Roboto Mono", monospace);
210
- font-size: 0.94rem;
211
- line-height: 1.45;
212
- }
213
-
214
- #chat-input::-webkit-scrollbar {
215
- width: 6px;
216
- height: 6px;
217
- }
218
-
219
- #chat-input::-webkit-scrollbar-track {
220
- background: transparent;
221
- margin: 4px 0;
222
- border-radius: 6px;
223
- }
224
-
225
- #chat-input::-webkit-scrollbar-thumb {
226
- background-color: rgba(155, 155, 155, 0.5);
227
- border-radius: 6px;
228
- -webkit-transition: background-color 0.2s ease;
229
- transition: background-color 0.2s ease;
230
- }
231
-
232
- #chat-input::-webkit-scrollbar-thumb:hover {
233
- background-color: rgba(155, 155, 155, 0.7);
234
- }
235
-
236
- #chat-input:focus {
237
- outline: none;
238
- background-color: transparent;
239
- }
240
-
241
- /* Progress ghost text animation — gentle pulse on placeholder */
242
- #chat-input.progress-active.is-empty::before {
243
- animation: placeholder-pulse 2s ease-in-out infinite;
244
- }
245
-
246
- @keyframes placeholder-pulse {
247
-
248
- 0%,
249
- 100% {
250
- opacity: 0.45;
251
- }
252
-
253
- 50% {
254
- opacity: 0.85;
255
- }
256
- }
257
-
258
- #chat-input.progress-active {
259
- border-color: color-mix(in srgb, var(--color-highlight) 20%, transparent);
260
- outline-color: color-mix(in srgb, var(--color-highlight) 15%, transparent);
261
- }
262
-
263
- #chat-input-progress-placeholder {
264
- position: absolute;
265
- top: 0;
266
- bottom: 0;
267
- left: 0.12rem;
268
- right: 2rem;
269
- color: var(--color-text-muted);
270
- opacity: 0.7;
271
- font-family: var(--font-family-code, "Roboto Mono", monospace);
272
- font-size: 0.9rem;
273
- line-height: 1.25;
274
- white-space: nowrap;
275
- overflow: hidden;
276
- text-overflow: ellipsis;
277
- pointer-events: none;
278
- display: flex;
279
- align-items: center;
280
- gap: 0.35rem;
281
- }
282
-
283
- #chat-input-progress-placeholder.progress-active {
284
- animation: placeholder-pulse 2s ease-in-out infinite;
285
- }
286
-
287
- #chat-input-progress-placeholder .chat-input-progress-icon {
288
- display: inline-flex;
289
- align-items: center;
290
- justify-content: center;
291
- font-size: 1.15rem;
292
- line-height: 1;
293
- color: var(--color-highlight);
294
- }
295
-
296
- #expand-button {
297
- position: absolute;
298
- top: 50%;
299
- right: 0;
300
- width: 2rem;
301
- height: 2rem;
302
- background: transparent;
303
- border: none;
304
- cursor: pointer;
305
- transform: translateY(-50%) scale(0.8);
306
- transform-origin: center;
307
- font-size: 1.2rem;
308
- color: var(--color-text);
309
- opacity: 0.2;
310
- transition: opacity 0.2s;
311
- display: flex;
312
- align-items: center;
313
- justify-content: center;
314
- padding: 0;
315
- }
316
-
317
- #expand-button:hover {
318
- opacity: 0.7;
319
- }
320
-
321
- #expand-button:active {
322
- opacity: 1;
323
- }
324
-
325
- #expand-button .material-symbols-outlined {
326
- display: block;
327
- font-size: 1.3rem;
328
- line-height: 1;
329
- }
330
-
331
- /* Chat buttons (Send/Mic) */
332
- #chat-buttons-wrapper {
333
- gap: 0.58rem;
334
- padding-left: 0;
335
- line-height: 1;
336
- display: -webkit-flex;
337
- display: flex;
338
- align-items: center;
339
- }
340
-
341
- .chat-button {
342
- border: 1px solid transparent;
343
- border-radius: 50%;
344
- color: #fff;
345
- cursor: pointer;
346
- font-size: var(--font-size-normal);
347
- height: 3.15rem;
348
- width: 3.15rem;
349
- margin: 0;
350
- display: -webkit-flex;
351
- display: flex;
352
- align-items: center;
353
- justify-content: center;
354
- flex-shrink: 0;
355
- flex-grow: 0;
356
- min-width: 3.15rem;
357
- -webkit-transition: background-color var(--transition-speed), border-color 0.16s ease, box-shadow 0.16s ease, filter 0.12s ease-in-out, transform 0.12s ease;
358
- transition: background-color var(--transition-speed), border-color 0.16s ease, box-shadow 0.16s ease, filter 0.12s ease-in-out, transform 0.12s ease;
359
- }
360
-
361
- #send-button {
362
- order: 2;
363
- border-radius: 12px;
364
- background-color: #4248f1;
365
- box-shadow: none;
366
- }
367
-
368
- #send-button.send-queue {
369
- background-color: #e67e22;
370
- box-shadow: none;
371
- }
372
-
373
- #send-button.stop {
374
- background-color: var(--color-error-text);
375
- box-shadow: none;
376
- }
377
-
378
- #send-button:hover {
379
- background-color: #353bc5;
380
- filter: none;
381
- box-shadow: none;
382
- }
383
-
384
- #send-button.send-queue:hover {
385
- background-color: #d35400;
386
- filter: none;
387
- }
388
-
389
- #send-button.stop:hover {
390
- background-color: color-mix(in srgb, var(--color-error-text) 82%, black);
391
- filter: none;
392
- }
393
-
394
- #send-button:active {
395
- background-color: #2b309c;
396
- transform: translateY(1px) scale(0.98);
397
- filter: brightness(0.96);
398
- }
399
-
400
- #send-button.stop:active {
401
- background-color: color-mix(in srgb, var(--color-error-text) 68%, black);
402
- }
403
-
404
- .chat-button svg {
405
- width: 1.5rem;
406
- height: 1.5rem;
407
- }
408
-
409
- #send-button .material-symbols-outlined {
410
- font-size: 1.78rem;
411
- }
412
-
413
- /* Responsive tweaks */
414
- @media (max-width: 640px) {
415
- #chat-input {
416
- align-content: center;
417
- }
418
-
419
- #chat-buttons-wrapper {
420
- display: flex;
421
- gap: 0.42rem;
422
- padding: 0 !important;
423
- width: auto;
424
- flex-wrap: nowrap;
425
- -webkit-transition: all 0.3s ease;
426
- transition: all 0.3s ease;
427
- }
428
-
429
- .input-row {
430
- gap: 0.42rem;
431
- min-height: 4rem;
432
- padding: 0.36rem 0.42rem 0.36rem 0.48rem;
433
- }
434
-
435
- .chat-more-trigger {
436
- width: 2.15rem;
437
- height: 2.15rem;
438
- }
439
-
440
- .chat-button {
441
- width: 2.75rem;
442
- height: 2.75rem;
443
- min-width: 2.75rem;
444
- }
445
- }
446
-
447
- @media (min-width: 768px) {
448
- #chat-buttons-wrapper {
449
- flex-wrap: nowrap;
450
- -webkit-flex-wrap: nowrap;
451
- flex-wrap: nowrap;
452
- }
453
- }
454
- </style>
455
- </body>
456
-
457
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c828481f4740f859a1157593e3c67a48a28696c7df876ab3e133d102cc369822
3
+ size 12869
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/chat-bar.html CHANGED
@@ -1,52 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store } from "/components/chat/input/input-store.js";
5
- import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
6
- </script>
7
- </head>
8
- <body>
9
- <div id="input-section" x-data>
10
- <x-extension id="chat-input-start"></x-extension>
11
- <template x-if="$store.chatInput">
12
- <div style="width: 100%; display: contents;">
13
- <!-- Message Queue section -->
14
- <x-component path="chat/message-queue/message-queue.html"></x-component>
15
-
16
- <!-- Attachment Preview section -->
17
- <div>
18
- <x-component path="chat/attachments/inputPreview.html" />
19
- </div>
20
-
21
- <x-component path="chat/input/chat-bar-input.html"></x-component>
22
- <x-component path="chat/input/bottom-actions-bar.html"></x-component>
23
- </div>
24
- </template>
25
- <x-extension id="chat-input-end"></x-extension>
26
- </div>
27
-
28
-
29
- <style>
30
- /* Input section wrapper */
31
- #input-section {
32
- position: relative;
33
- background-color: var(--color-panel);
34
- display: -webkit-flex;
35
- display: flex;
36
- flex-direction: column;
37
- gap: var(--spacing-xs);
38
- padding: var(--spacing-xxs) var(--spacing-md) var(--spacing-sm) 0.8rem;
39
- align-items: start;
40
- place-items: normal;
41
- flex-shrink: 0;
42
- overflow: visible;
43
- z-index: 1001;
44
- }
45
-
46
- @media (max-width: 768px) {
47
- #input-section { align-items: normal !important; }
48
- }
49
-
50
- </style>
51
- </body>
52
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b9ccb53f0c73e706ec6c81d539e42adb8803c904bd7926055afbd0298d6d34a
3
+ size 1609
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/composer-banner-store.js CHANGED
@@ -1,69 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import { callJsonApi } from "/js/api.js";
3
- import { getCurrentUserISOString } from "/js/time-utils.js";
4
-
5
- function buildBannersContext() {
6
- return {
7
- url: window.location.href,
8
- protocol: window.location.protocol,
9
- hostname: window.location.hostname,
10
- port: window.location.port,
11
- browser: navigator.userAgent,
12
- timestamp: getCurrentUserISOString(),
13
- };
14
- }
15
-
16
- export const store = createStore("composerBanner", {
17
- missingApiKeys: [],
18
- hasMissingApiKeyBanner: false,
19
- loading: false,
20
- lastRefresh: 0,
21
- _modalCloseBound: false,
22
-
23
- get hasMissingApiKeys() {
24
- return this.hasMissingApiKeyBanner;
25
- },
26
-
27
- get missingApiKeysSummaryText() {
28
- if (!this.hasMissingApiKeys) return "";
29
- if (!Array.isArray(this.missingApiKeys) || this.missingApiKeys.length === 0) {
30
- return "Configure your model provider API keys to continue.";
31
- }
32
- return this.missingApiKeys
33
- .map((p) => `${p.model_type} (${p.provider})`)
34
- .join(", ");
35
- },
36
-
37
- init() {
38
- if (this._modalCloseBound) return;
39
- this._modalCloseBound = true;
40
- document.addEventListener("modal-closed", () => {
41
- this.refresh(true);
42
- });
43
- },
44
-
45
- async refresh(force = false) {
46
- const now = Date.now();
47
- if (!force && now - this.lastRefresh < 1000) return;
48
- this.lastRefresh = now;
49
- this.loading = true;
50
- try {
51
- const response = await callJsonApi("/banners", {
52
- banners: [],
53
- context: buildBannersContext(),
54
- });
55
- const banners = Array.isArray(response?.banners) ? response.banners : [];
56
- const missingApiKeyBanner = banners.find((banner) => banner?.id === "missing-api-key");
57
- this.hasMissingApiKeyBanner = !!missingApiKeyBanner;
58
- this.missingApiKeys = Array.isArray(missingApiKeyBanner?.missing_providers)
59
- ? missingApiKeyBanner.missing_providers
60
- : [];
61
- } catch (e) {
62
- console.error("composerBanner refresh failed", e);
63
- this.hasMissingApiKeyBanner = false;
64
- this.missingApiKeys = [];
65
- } finally {
66
- this.loading = false;
67
- }
68
- },
69
- });
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f75dbff9419edd621acc868de8a904fd8e910e89555a68a9eadedb8e9562a8c2
3
+ size 2088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/input-store.js CHANGED
@@ -1,780 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import * as shortcuts from "/js/shortcuts.js";
3
- import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
4
- import { openLatest as openLatestSurface } from "/js/surfaces.js";
5
- import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
6
- import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
7
- import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
8
-
9
- const ICON_MARKER_RE = /icon:\/\/([a-zA-Z0-9_]+)(\[(?:\\.|[^\]])*\])?/g;
10
- const FENCE_LINE_RE = /^```([A-Za-z0-9_-]*)?$/;
11
- const BLOCK_TAGS = new Set(["DIV", "P", "LI"]);
12
- const DRAFT_STORAGE_PREFIX = "a0:chat-draft:";
13
-
14
- function escapeHTML(value) {
15
- return String(value ?? "")
16
- .replace(/&/g, "&amp;")
17
- .replace(/</g, "&lt;")
18
- .replace(/>/g, "&gt;")
19
- .replace(/"/g, "&quot;")
20
- .replace(/'/g, "&#039;");
21
- }
22
-
23
- function unescapeIconTooltip(block) {
24
- if (!block) return "";
25
- return block
26
- .slice(1, -1)
27
- .replace(/\\\[/g, "[")
28
- .replace(/\\\]/g, "]")
29
- .replace(/\\\\/g, "\\");
30
- }
31
-
32
- function convertIconMarkersToHtml(value) {
33
- const text = String(value ?? "");
34
- let html = "";
35
- let lastIndex = 0;
36
-
37
- text.replace(ICON_MARKER_RE, (match, iconName, tooltipBlock, offset) => {
38
- html += escapeHTML(text.slice(lastIndex, offset));
39
- const tooltip = unescapeIconTooltip(tooltipBlock) || iconName;
40
- html += (
41
- `<x-icon class="icon chat-input-progress-icon" ` +
42
- `title="${escapeHTML(tooltip)}" name="${escapeHTML(iconName)}"></x-icon>`
43
- );
44
- lastIndex = offset + match.length;
45
- return match;
46
- });
47
-
48
- html += escapeHTML(text.slice(lastIndex));
49
- return html;
50
- }
51
-
52
- const model = {
53
- paused: false,
54
- _message: "",
55
- _editorEl: null,
56
- _history: [],
57
- _historyIndex: null,
58
- _draft: "",
59
- _historyCtxid: null,
60
- _draftCtxid: null,
61
- /** Composer + menu (bottom actions moved into dropdown) */
62
- chatMoreMenuOpen: false,
63
- progressText: "",
64
- progressActive: false,
65
-
66
- get message() {
67
- return this._message;
68
- },
69
-
70
- set message(value) {
71
- this._message = String(value ?? "");
72
- this._renderEditorFromText(this._message);
73
- this._saveDraft();
74
- },
75
-
76
- toggleChatMoreMenu() {
77
- this.chatMoreMenuOpen = !this.chatMoreMenuOpen;
78
- },
79
-
80
- closeChatMoreMenu() {
81
- this.chatMoreMenuOpen = false;
82
- },
83
-
84
- _getSendState() {
85
- const hasInput = this.message.trim() || attachmentsStore?.attachments?.length > 0;
86
- const hasQueue = !!messageQueueStore?.hasQueue;
87
- const running = !!chatsStore.selectedContext?.running;
88
-
89
- if (running && !hasInput) return "stop";
90
- if (hasQueue && !hasInput) return "all";
91
- if ((running || hasQueue) && hasInput) return "queue";
92
- return "normal";
93
- },
94
-
95
- get inputPlaceholder() {
96
- if (!chatsStore.selected) return "Ask anything to start a new chat";
97
- const state = this._getSendState();
98
- if ((state === "all" || state === "stop") && messageQueueStore?.hasQueue) {
99
- return "Press Enter to send queued messages";
100
- }
101
- if (this.showProgressPlaceholder) return "";
102
- return "Type your message here...";
103
- },
104
-
105
- get showProgressPlaceholder() {
106
- const state = this._getSendState();
107
- return (
108
- !!chatsStore.selected &&
109
- state !== "all" &&
110
- !(state === "stop" && messageQueueStore?.hasQueue) &&
111
- !!this.progressText &&
112
- !this.message
113
- );
114
- },
115
-
116
- get progressPlaceholderHtml() {
117
- return convertIconMarkersToHtml(this.progressText);
118
- },
119
-
120
- // Computed: send button icon type
121
- get sendButtonIcon() {
122
- const state = this._getSendState();
123
- if (state === "stop") return "stop";
124
- if (state === "all") return "send_and_archive";
125
- if (state === "queue") return "schedule_send";
126
- return "arrow_forward";
127
- },
128
-
129
- // Computed: send button CSS class
130
- get sendButtonClass() {
131
- const state = this._getSendState();
132
- if (state === "stop") return "stop";
133
- if (state === "all") return "send-queue send-all";
134
- if (state === "queue") return "send-queue queue";
135
- return "";
136
- },
137
-
138
- // Computed: send button title
139
- get sendButtonTitle() {
140
- const state = this._getSendState();
141
- if (state === "stop") return "Stop agent";
142
- if (state === "all") return "Send all queued messages";
143
- if (state === "queue") return "Add to queue";
144
- return "Send message";
145
- },
146
-
147
- init() {
148
- console.log("Input store initialized");
149
- // Event listeners are now handled via Alpine directives in the component
150
- },
151
-
152
- async sendMessage() {
153
- this._syncMessageFromEditor();
154
- const pendingMessage = this.message;
155
-
156
- if (!chatsStore.selected && (this.message.trim() || attachmentsStore?.attachments?.length > 0)) {
157
- const ctxid = await chatsStore.newChat();
158
- if (!ctxid && !chatsStore.selected) return;
159
- this.message = pendingMessage;
160
- }
161
-
162
- // Capture sent prompt to per-chat history (bash-style)
163
- try { this._pushHistory(this.message); } catch (_e) { /* ignore */ }
164
-
165
- // Delegate to the global function
166
- if (globalThis.sendMessage) {
167
- await globalThis.sendMessage();
168
- }
169
- },
170
-
171
- async activateSendButton() {
172
- this._syncMessageFromEditor();
173
- if (this._getSendState() === "stop") {
174
- await this.stopAgent();
175
- return;
176
- }
177
- await this.sendMessage();
178
- },
179
-
180
- mountEditor(editor) {
181
- this._editorEl = editor;
182
- this.setDraftContext(shortcuts.getCurrentContextId());
183
- this._renderEditorFromText(this._message);
184
- this.adjustTextareaHeight({ target: editor });
185
- },
186
-
187
- unmountEditor(editor) {
188
- if (this._editorEl === editor) this._editorEl = null;
189
- },
190
-
191
- _composerTextareas(target = null) {
192
- const editor = target?.closest?.("#chat-input") || this._editorEl || document.getElementById("chat-input");
193
- return editor ? [editor] : [];
194
- },
195
-
196
- _activeTextarea() {
197
- const active = document.activeElement;
198
- const activeEditor = active?.closest?.("#chat-input");
199
- if (activeEditor) {
200
- return activeEditor;
201
- }
202
- return this._editorEl || document.getElementById("chat-input");
203
- },
204
-
205
- _renderEditorFromText(text) {
206
- const editor = this._editorEl;
207
- if (!editor) return;
208
- if (editor.textContent !== text || editor.querySelector("[data-code-block]")) {
209
- editor.textContent = text;
210
- }
211
- this._setEditorEmptyState();
212
- },
213
-
214
- _setEditorEmptyState() {
215
- const editor = this._editorEl;
216
- if (editor) editor.classList.toggle("is-empty", !this._message);
217
- },
218
-
219
- _createCodeBlock(lang = "") {
220
- const block = document.createElement("div");
221
- block.className = "composer-code-block";
222
- block.dataset.codeBlock = "true";
223
- block.dataset.lang = lang;
224
- block.contentEditable = "false";
225
-
226
- const code = document.createElement("pre");
227
- code.className = "composer-code-content";
228
- code.dataset.codeContent = "true";
229
- code.contentEditable = "true";
230
- code.spellcheck = false;
231
- code.setAttribute("aria-label", "Code block");
232
- block.append(code);
233
-
234
- return block;
235
- },
236
-
237
- _plainText(node) {
238
- if (!node) return "";
239
- if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || "";
240
- if (node.nodeType !== Node.ELEMENT_NODE) return "";
241
- if (node.matches?.("[data-code-block]")) {
242
- const lang = node.dataset.lang || "";
243
- const code = this._plainText(node.querySelector("[data-code-content]")).replace(/\n$/, "");
244
- return "```" + lang + "\n" + code + "\n```";
245
- }
246
- if (node.tagName === "BR") return "\n";
247
-
248
- let text = "";
249
- for (const child of node.childNodes) text += this._plainText(child);
250
- if (node !== this._editorEl && BLOCK_TAGS.has(node.tagName) && text && !text.endsWith("\n")) {
251
- text += "\n";
252
- }
253
- return text;
254
- },
255
-
256
- _editorToMarkdown() {
257
- return this._plainText(this._editorEl).replace(/\n$/, "");
258
- },
259
-
260
- _syncMessageFromEditor() {
261
- if (!this._editorEl) return;
262
- this._message = this._editorToMarkdown();
263
- this._setEditorEmptyState();
264
- this._saveDraft();
265
- },
266
-
267
- _isInCodeBlock(target) {
268
- return Boolean(target?.closest?.("[data-code-content]"));
269
- },
270
-
271
- _selectionOffsets(editor) {
272
- const selection = document.getSelection?.();
273
- if (!selection || selection.rangeCount === 0 || !editor) {
274
- return { start: this._message.length, end: this._message.length };
275
- }
276
- const range = selection.getRangeAt(0);
277
- if (!editor.contains(range.startContainer) || !editor.contains(range.endContainer)) {
278
- return { start: this._message.length, end: this._message.length };
279
- }
280
-
281
- const before = range.cloneRange();
282
- before.selectNodeContents(editor);
283
- before.setEnd(range.startContainer, range.startOffset);
284
-
285
- const selected = range.cloneRange();
286
- return {
287
- start: before.toString().length,
288
- end: before.toString().length + selected.toString().length,
289
- };
290
- },
291
-
292
- _setEditorCaret(offset) {
293
- const editor = this._activeTextarea();
294
- const selection = document.getSelection?.();
295
- if (!editor || !selection) return;
296
-
297
- editor.focus();
298
- const range = document.createRange();
299
- let remaining = Math.max(0, Number(offset) || 0);
300
- const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
301
- let node = walker.nextNode();
302
-
303
- while (node) {
304
- const length = node.nodeValue.length;
305
- if (remaining <= length) {
306
- range.setStart(node, remaining);
307
- range.collapse(true);
308
- selection.removeAllRanges();
309
- selection.addRange(range);
310
- return;
311
- }
312
- remaining -= length;
313
- node = walker.nextNode();
314
- }
315
-
316
- range.selectNodeContents(editor);
317
- range.collapse(false);
318
- selection.removeAllRanges();
319
- selection.addRange(range);
320
- },
321
-
322
- _focusCodeBlock(block) {
323
- queueMicrotask(() => {
324
- const code = block.querySelector("[data-code-content]");
325
- const selection = document.getSelection?.();
326
- if (!code || !selection) return;
327
- code.focus();
328
- const range = document.createRange();
329
- range.selectNodeContents(code);
330
- range.collapse(false);
331
- selection.removeAllRanges();
332
- selection.addRange(range);
333
- });
334
- },
335
-
336
- _insertPlainText(text) {
337
- const selection = document.getSelection?.();
338
- if (!selection || selection.rangeCount === 0) return false;
339
- const range = selection.getRangeAt(0);
340
- range.deleteContents();
341
- const node = document.createTextNode(text);
342
- range.insertNode(node);
343
- range.setStart(node, node.nodeValue.length);
344
- range.collapse(true);
345
- selection.removeAllRanges();
346
- selection.addRange(range);
347
- return true;
348
- },
349
-
350
- _tryCreateCodeBlock($event) {
351
- const editor = this._activeTextarea();
352
- const selection = document.getSelection?.();
353
- if (!editor || !selection || selection.rangeCount === 0) return false;
354
-
355
- const range = selection.getRangeAt(0);
356
- if (!range.collapsed || !editor.contains(range.startContainer)) return false;
357
- if (this._isInCodeBlock(range.startContainer.parentElement)) return false;
358
-
359
- const before = range.cloneRange();
360
- before.selectNodeContents(editor);
361
- before.setEnd(range.startContainer, range.startOffset);
362
-
363
- const after = range.cloneRange();
364
- after.selectNodeContents(editor);
365
- after.setStart(range.startContainer, range.startOffset);
366
-
367
- const lineBefore = before.toString().split("\n").pop() || "";
368
- const lineAfter = (after.toString().split("\n")[0] || "");
369
- const match = (lineBefore + lineAfter).match(FENCE_LINE_RE);
370
- if (!match || lineAfter) return false;
371
- if (range.startContainer.nodeType !== Node.TEXT_NODE || range.startOffset < lineBefore.length) return false;
372
-
373
- $event.preventDefault();
374
- const editRange = range.cloneRange();
375
- editRange.setStart(range.startContainer, range.startOffset - lineBefore.length);
376
- editRange.deleteContents();
377
-
378
- const block = this._createCodeBlock(match[1] || "");
379
- editRange.insertNode(block);
380
- block.after(document.createTextNode("\n"));
381
- this._focusCodeBlock(block);
382
- this._syncMessageFromEditor();
383
- this.adjustTextareaHeight({ target: editor });
384
- return true;
385
- },
386
-
387
- handleInput($event) {
388
- this._syncMessageFromEditor();
389
- this.adjustTextareaHeight($event);
390
- },
391
-
392
- handlePaste($event) {
393
- const text = $event.clipboardData?.getData("text/plain");
394
- if (text === undefined) return;
395
- $event.preventDefault();
396
- this._insertPlainText(text);
397
- this.handleInput($event);
398
- },
399
-
400
- handleKeydown($event) {
401
- if ($event.isComposing || $event.keyCode === 229) return;
402
-
403
- if ($event.key === "Enter") {
404
- if (this._isInCodeBlock($event.target)) return;
405
- if (!$event.shiftKey) {
406
- if (this._tryCreateCodeBlock($event)) return;
407
- $event.preventDefault();
408
- this.sendMessage();
409
- }
410
- return;
411
- }
412
-
413
- if ($event.key === "ArrowUp") this.historyPrev($event);
414
- if ($event.key === "ArrowDown") this.historyNext($event);
415
- },
416
-
417
- adjustTextareaHeight($event = null) {
418
- const target = $event?.target || null;
419
- for (const chatInput of this._composerTextareas(target)) {
420
- chatInput.style.height = "auto";
421
- chatInput.style.height = chatInput.scrollHeight + "px";
422
- // pick up any layout shift triggered by the height assignment
423
- chatInput.style.height = Math.max(chatInput.scrollHeight, parseInt(chatInput.style.height)) + "px";
424
- }
425
- },
426
-
427
- async pauseAgent(paused) {
428
- const prev = this.paused;
429
- this.paused = paused;
430
- try {
431
- const context = globalThis.getContext?.();
432
- if (!globalThis.sendJsonData)
433
- throw new Error("sendJsonData not available");
434
- await globalThis.sendJsonData("/pause", { paused, context });
435
- } catch (e) {
436
- this.paused = prev;
437
- if (globalThis.toastFetchError) {
438
- globalThis.toastFetchError("Error pausing agent", e);
439
- }
440
- }
441
- },
442
-
443
- async stopAgent() {
444
- try {
445
- const context = globalThis.getContext?.();
446
- if (!context || !globalThis.sendJsonData) return;
447
- await globalThis.sendJsonData("/stop", { context });
448
- } catch (e) {
449
- if (globalThis.toastFetchError) {
450
- globalThis.toastFetchError("Error stopping agent", e);
451
- }
452
- }
453
- },
454
-
455
- async nudge() {
456
- try {
457
- const context = globalThis.getContext();
458
- await globalThis.sendJsonData("/nudge", { ctxid: context });
459
- } catch (e) {
460
- if (globalThis.toastFetchError) {
461
- globalThis.toastFetchError("Error nudging agent", e);
462
- }
463
- }
464
- },
465
-
466
- async loadKnowledge() {
467
- try {
468
- const resp = await shortcuts.callJsonApi(
469
- "/plugins/_memory/knowledge_path_get",
470
- { ctxid: shortcuts.getCurrentContextId() }
471
- );
472
- if (!resp.ok) throw new Error("Error getting knowledge path");
473
- const path = resp.path;
474
-
475
- // open file browser and wait for it to close
476
- await fileBrowserStore.open(path);
477
-
478
- // progress notification
479
- shortcuts.frontendNotification({
480
- type: shortcuts.NotificationType.PROGRESS,
481
- message: "Loading knowledge...",
482
- priority: shortcuts.NotificationPriority.NORMAL,
483
- displayTime: 999,
484
- group: "knowledge_load",
485
- frontendOnly: true,
486
- });
487
-
488
- // then reindex knowledge
489
- await globalThis.sendJsonData("/plugins/_memory/knowledge_reindex", {
490
- ctxid: shortcuts.getCurrentContextId(),
491
- });
492
-
493
- // finished notification
494
- shortcuts.frontendNotification({
495
- type: shortcuts.NotificationType.SUCCESS,
496
- message: "Knowledge loaded successfully",
497
- priority: shortcuts.NotificationPriority.NORMAL,
498
- displayTime: 2,
499
- group: "knowledge_load",
500
- frontendOnly: true,
501
- });
502
- } catch (e) {
503
- // error notification
504
- shortcuts.frontendNotification({
505
- type: shortcuts.NotificationType.ERROR,
506
- message: "Error loading knowledge",
507
- priority: shortcuts.NotificationPriority.NORMAL,
508
- displayTime: 5,
509
- group: "knowledge_load",
510
- frontendOnly: true,
511
- });
512
- }
513
- },
514
-
515
- // previous implementation without projects
516
- async _loadKnowledge() {
517
- const input = document.createElement("input");
518
- input.type = "file";
519
- input.accept = ".txt,.pdf,.csv,.html,.json,.md";
520
- input.multiple = true;
521
-
522
- input.onchange = async () => {
523
- try {
524
- const formData = new FormData();
525
- for (let file of input.files) {
526
- formData.append("files[]", file);
527
- }
528
-
529
- formData.append("ctxid", globalThis.getContext());
530
-
531
- const response = await globalThis.fetchApi("/import_knowledge", {
532
- method: "POST",
533
- body: formData,
534
- });
535
-
536
- if (!response.ok) {
537
- if (globalThis.toast)
538
- globalThis.toast(await response.text(), "error");
539
- } else {
540
- const data = await response.json();
541
- if (globalThis.toast) {
542
- globalThis.toast(
543
- "Knowledge files imported: " + data.filenames.join(", "),
544
- "success"
545
- );
546
- }
547
- }
548
- } catch (e) {
549
- if (globalThis.toastFetchError) {
550
- globalThis.toastFetchError("Error loading knowledge", e);
551
- }
552
- }
553
- };
554
-
555
- input.click();
556
- },
557
-
558
- async browseFiles(path) {
559
- if (!path) {
560
- const ctxid = shortcuts.getCurrentContextId();
561
-
562
- if (ctxid) {
563
- try {
564
- const resp = await shortcuts.callJsonApi("/chat_files_path_get", {
565
- ctxid,
566
- });
567
- if (resp.ok) path = resp.path;
568
- } catch (_e) {
569
- console.error("Error getting chat files path", _e);
570
- }
571
- }
572
- }
573
- let opened = false;
574
- try {
575
- opened = await openLatestSurface("files", { path, source: "sidebar" });
576
- } catch (error) {
577
- console.error("Error opening Files surface", error);
578
- }
579
- if (!opened) await fileBrowserStore.open(path);
580
- },
581
-
582
- focus() {
583
- const chatInput = this._activeTextarea();
584
- if (chatInput) {
585
- chatInput.focus();
586
- }
587
- },
588
-
589
- setDraftContext(ctxid) {
590
- const nextCtxid = String(ctxid || "");
591
- if (nextCtxid === this._draftCtxid) return;
592
- if (this._draftCtxid !== null) this._syncMessageFromEditor();
593
-
594
- this._draftCtxid = nextCtxid;
595
- this._historyIndex = null;
596
- this._draft = "";
597
-
598
- let draft = "";
599
- if (nextCtxid) {
600
- try { draft = sessionStorage.getItem(DRAFT_STORAGE_PREFIX + nextCtxid) || ""; } catch (_e) { /* ignore */ }
601
- }
602
- this._message = draft;
603
- this._renderEditorFromText(draft);
604
- queueMicrotask(() => this.adjustTextareaHeight());
605
- },
606
-
607
- _saveDraft() {
608
- if (!this._draftCtxid) return;
609
- try {
610
- const key = DRAFT_STORAGE_PREFIX + this._draftCtxid;
611
- if (this._message) sessionStorage.setItem(key, this._message);
612
- else sessionStorage.removeItem(key);
613
- } catch (_e) { /* ignore unavailable storage */ }
614
- },
615
-
616
- _loadHistory() {
617
- let ctxid = null;
618
- try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
619
- this._historyCtxid = ctxid;
620
- this._history = [];
621
- this._historyIndex = null;
622
- this._draft = "";
623
- if (!ctxid) return;
624
- let raw = null;
625
- try { raw = localStorage.getItem("a0:chat-history:" + ctxid); } catch (_e) { raw = null; }
626
- if (raw !== null) {
627
- try {
628
- const arr = JSON.parse(raw);
629
- if (Array.isArray(arr)) {
630
- this._history = arr.filter((s) => typeof s === "string");
631
- }
632
- } catch (_e) { /* ignore */ }
633
- return;
634
- }
635
- // No entry yet for this chat: seed from rendered chat DOM (one-time bootstrap)
636
- try {
637
- const seeded = this._seedFromChatDom();
638
- if (seeded.length > 0) {
639
- this._history = seeded;
640
- this._saveHistory();
641
- } else {
642
- // Persist an empty array so we don't re-seed on every nav; respects user clearing
643
- this._saveHistory();
644
- }
645
- } catch (_e) { /* ignore */ }
646
- },
647
-
648
- _seedFromChatDom() {
649
- const out = [];
650
- let nodes;
651
- try {
652
- nodes = document.querySelectorAll(".user-container .message-user .message-text pre");
653
- } catch (_e) {
654
- return out;
655
- }
656
- for (const pre of nodes) {
657
- const text = (pre.textContent || "").trim();
658
- if (!text) continue;
659
- if (out.length > 0 && out[out.length - 1] === text) continue; // ignoredups
660
- out.push(text);
661
- }
662
- if (out.length > 50) return out.slice(-50);
663
- return out;
664
- },
665
-
666
- _saveHistory() {
667
- if (!this._historyCtxid) return;
668
- try {
669
- localStorage.setItem(
670
- "a0:chat-history:" + this._historyCtxid,
671
- JSON.stringify(this._history)
672
- );
673
- } catch (_e) { /* ignore quota / disabled */ }
674
- },
675
-
676
- _ensureHistoryLoaded() {
677
- let ctxid = null;
678
- try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
679
- if (ctxid !== this._historyCtxid) {
680
- this._loadHistory();
681
- }
682
- },
683
-
684
- _pushHistory(text) {
685
- if (typeof text !== "string") return;
686
- const trimmed = text.trim();
687
- if (!trimmed) return;
688
- this._ensureHistoryLoaded();
689
- if (this._history.length > 0 && this._history[this._history.length - 1] === trimmed) {
690
- this._historyIndex = null;
691
- this._draft = "";
692
- return;
693
- }
694
- this._history.push(trimmed);
695
- if (this._history.length > 50) {
696
- this._history = this._history.slice(-50);
697
- }
698
- this._saveHistory();
699
- this._historyIndex = null;
700
- this._draft = "";
701
- },
702
-
703
- _setCaretStart() {
704
- queueMicrotask(() => {
705
- const editor = this._activeTextarea();
706
- if (editor) {
707
- this._setEditorCaret(0);
708
- try { editor.scrollTop = 0; } catch (_e) { /* ignore */ }
709
- }
710
- this.adjustTextareaHeight();
711
- });
712
- },
713
-
714
- _setCaretEnd() {
715
- queueMicrotask(() => {
716
- const editor = this._activeTextarea();
717
- if (editor) {
718
- this._setEditorCaret(editor.innerText.length);
719
- try { editor.scrollTop = editor.scrollHeight; } catch (_e) { /* ignore */ }
720
- }
721
- this.adjustTextareaHeight();
722
- });
723
- },
724
-
725
- historyPrev($event) {
726
- if ($event && ($event.isComposing || $event.keyCode === 229)) return;
727
- if (this._isInCodeBlock($event?.target)) return;
728
- const editor = this._activeTextarea();
729
- if (!editor) return;
730
- const { start, end } = this._selectionOffsets(editor);
731
- if (start !== 0 || end !== 0) return;
732
- $event.preventDefault();
733
- this._ensureHistoryLoaded();
734
- if (this._history.length === 0) return;
735
- if (this._historyIndex === null) {
736
- this._draft = this.message || "";
737
- this._historyIndex = this._history.length - 1;
738
- } else if (this._historyIndex > 0) {
739
- this._historyIndex -= 1;
740
- } else {
741
- return;
742
- }
743
- this.message = this._history[this._historyIndex];
744
- this._setCaretStart();
745
- },
746
-
747
- historyNext($event) {
748
- if ($event && ($event.isComposing || $event.keyCode === 229)) return;
749
- if (this._isInCodeBlock($event?.target)) return;
750
- const editor = this._activeTextarea();
751
- if (!editor) return;
752
- const { start, end } = this._selectionOffsets(editor);
753
- const valueLength = editor.innerText.length;
754
- if (start !== valueLength || end !== valueLength) return;
755
- $event.preventDefault();
756
- if (this._historyIndex === null) return;
757
- if (this._historyIndex < this._history.length - 1) {
758
- this._historyIndex += 1;
759
- this.message = this._history[this._historyIndex];
760
- } else {
761
- this._historyIndex = null;
762
- this.message = this._draft || "";
763
- this._draft = "";
764
- }
765
- this._setCaretEnd();
766
- },
767
-
768
- reset() {
769
- this.message = "";
770
- attachmentsStore.clearAttachments();
771
- this.chatMoreMenuOpen = false;
772
- this._historyIndex = null;
773
- this._draft = "";
774
- this.adjustTextareaHeight();
775
- }
776
- };
777
-
778
- const store = createStore("chatInput", model);
779
-
780
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f9248f9813e804fce49f05b0f922faf8638220648c5f1b399742a5a90babc56
3
+ size 23500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/input/progress.html CHANGED
@@ -1,57 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store as chatNavStore } from "/components/chat/navigation/chat-navigation-store.js";
5
- </script>
6
- </head>
7
- <body>
8
- <div id="progress-bar-box" x-data>
9
- <x-extension id="chat-input-progress-start"></x-extension>
10
-
11
- <!-- Hidden legacy progress-bar element (keeps JS updater happy, not displayed) -->
12
- <span id="progress-bar" style="display:none;"></span>
13
-
14
- <div id="progress-bar-right">
15
- <h4
16
- id="progress-bar-stop-speech"
17
- x-data="{
18
- speaking: window.ttsService?.isSpeaking?.() || false,
19
- _listener: null,
20
- init() {
21
- this._listener = (event) => {
22
- this.speaking = !!event?.detail?.isSpeaking;
23
- };
24
- window.ttsService?.addEventListener?.('statechange', this._listener);
25
- },
26
- cleanup() {
27
- if (this._listener) {
28
- window.ttsService?.removeEventListener?.('statechange', this._listener);
29
- }
30
- },
31
- }"
32
- x-init="init()"
33
- x-destroy="cleanup()"
34
- x-cloak
35
- x-show="speaking"
36
- >
37
- <span id="stop-speech" @click="window.ttsService?.stop?.()" style="cursor: pointer" title="Stop Speech" aria-label="Stop Speech">
38
- <x-icon class="icon" name="volume_off"></x-icon>
39
- </span>
40
- </h4>
41
- <x-extension id="chat-nav-after"></x-extension>
42
- </div>
43
- <x-extension id="chat-input-progress-end"></x-extension>
44
- </div>
45
-
46
-
47
- <style>
48
- #progress-bar-box { background-color: var(--color-panel); padding: var(--spacing-xs) var(--spacing-md) 0 var(--spacing-md); display: flex; flex-wrap: nowrap; justify-content: flex-start; align-items: center; z-index: 1001; gap: var(--spacing-sm); min-height: 0; }
49
- #progress-bar-box > x-extension { display: contents; }
50
- #progress-bar-box h4 { margin: 0; }
51
- #progress-bar-right { display: flex; align-items: center; gap: var(--spacing-sm); flex-shrink: 0; margin-left: auto;}
52
- #progress-bar-right > x-extension { display: contents; }
53
- .shiny-text { background: linear-gradient(to right, var(--color-primary-dark) 20%, var(--color-text) 40%, var(--color-text) 60%, var(--color-primary-dark) 60%); background-size: 200% auto; color: transparent; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; }
54
- @keyframes shine { to { background-position: -200% center; } }
55
- </style>
56
- </body>
57
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3090e2fa54cf5a9b86c50056486d5958eecbb28d88d1fb1b1d1195aeb6092f4
3
+ size 2596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/message-queue/message-queue-store.js CHANGED
@@ -1,245 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import { store as navStore } from "/components/chat/navigation/chat-navigation-store.js";
3
- import * as api from "/js/api.js";
4
- import {
5
- toastFrontendInfo,
6
- NotificationPriority,
7
- } from "/components/notifications/notification-store.js";
8
- import { sleep } from "/js/sleep.js";
9
- import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
10
-
11
- const model = {
12
- // items: [],
13
- get items() {
14
- return chatsStore.selectedContext?.message_queue || [];
15
- },
16
-
17
- pendingItems: [], // Local pending items (uploading to queue)
18
-
19
- _pendingAddOps: {},
20
-
21
- _lastAddToQueuePromise: Promise.resolve(),
22
-
23
- _getQueueScrollerEl() {
24
- return document.querySelector(".queue-preview .queue-items");
25
- },
26
-
27
- scrollQueueToBottom() {
28
- const el = this._getQueueScrollerEl();
29
- if (!el) return;
30
-
31
- const scroll = () => {
32
- el.scrollTop = el.scrollHeight;
33
- };
34
-
35
- requestAnimationFrame(() => {
36
- scroll();
37
- requestAnimationFrame(scroll);
38
- });
39
- },
40
-
41
- get hasQueue() {
42
- return this.items.length > 0 || this.pendingItems.length > 0;
43
- },
44
-
45
- get count() {
46
- return this.items.length + this.pendingItems.length;
47
- },
48
-
49
- // Combined items for display: confirmed first, then pending at the end
50
- get allItems() {
51
- return [...this.items, ...this.pendingItems];
52
- },
53
-
54
- async addToQueue(text, attachments = []) {
55
- const context = globalThis.getContext?.();
56
- if (!context) return false;
57
-
58
- // Generate a temporary ID for pending item
59
- const tempId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
60
- const pendingItem = {
61
- id: tempId,
62
- text: text.substring(0, 200) || "(attachment only)",
63
- attachments: attachments.map((a) => a.name || a.file?.name || "file"),
64
- pending: true,
65
- };
66
-
67
- const controller =
68
- typeof AbortController !== "undefined" ? new AbortController() : null;
69
- this._pendingAddOps = {
70
- ...this._pendingAddOps,
71
- [tempId]: {
72
- canceled: false,
73
- controller,
74
- },
75
- };
76
-
77
- // Add to pending immediately for UI feedback
78
- this.pendingItems = [...this.pendingItems, pendingItem];
79
- this.scrollQueueToBottom();
80
-
81
- const run = async () => {
82
- const op = this._pendingAddOps?.[tempId];
83
- if (!op || op.canceled) {
84
- this._pendingAddOps = { ...this._pendingAddOps };
85
- delete this._pendingAddOps[tempId];
86
- return false;
87
- }
88
-
89
- try {
90
- let filenames = [];
91
- if (attachments.length > 0) {
92
- const formData = new FormData();
93
- for (const att of attachments) {
94
- formData.append("file", att.file || att);
95
- }
96
- const resp = await api.fetchApi("/upload", {
97
- method: "POST",
98
- body: formData,
99
- signal: op.controller ? op.controller.signal : undefined,
100
- });
101
- if (resp.ok) {
102
- const result = await resp.json();
103
- filenames = result.filenames || [];
104
- }
105
- }
106
-
107
- const resp = await api.fetchApi("/message_queue_add", {
108
- method: "POST",
109
- headers: {
110
- "Content-Type": "application/json",
111
- },
112
- credentials: "same-origin",
113
- body: JSON.stringify({
114
- context,
115
- text,
116
- attachments: filenames,
117
- item_id: tempId,
118
- }),
119
- signal: op.controller ? op.controller.signal : undefined,
120
- });
121
-
122
- if (!resp || !resp.ok) {
123
- return false;
124
- }
125
-
126
- const response = await resp.json();
127
-
128
- return response?.ok || false;
129
- } catch (e) {
130
- if (e?.name !== "AbortError") {
131
- console.error("Failed to queue message:", e);
132
- }
133
- return false;
134
- } finally {
135
- this._pendingAddOps = { ...this._pendingAddOps };
136
- delete this._pendingAddOps[tempId];
137
- }
138
- };
139
-
140
- // Chain promises to ensure sequential execution
141
- const previous = this._lastAddToQueuePromise || Promise.resolve();
142
- const chained = previous.catch(() => false).then(run);
143
- this._lastAddToQueuePromise = chained.catch(() => false);
144
- return await chained;
145
- },
146
-
147
- async removeItem(itemId) {
148
- const context = globalThis.getContext?.();
149
- if (!context) return;
150
-
151
- const isPending = this.pendingItems.some((p) => p.id === itemId);
152
- if (isPending) {
153
- const op = this._pendingAddOps?.[itemId];
154
- if (op) {
155
- op.canceled = true;
156
- if (op.controller) {
157
- op.controller.abort();
158
- }
159
- }
160
- this.pendingItems = this.pendingItems.filter((p) => p.id !== itemId);
161
- return;
162
- }
163
-
164
- try {
165
- await api.callJsonApi("/message_queue_remove", {
166
- context,
167
- item_id: itemId,
168
- });
169
- } catch (e) {
170
- console.error("Failed to remove from queue:", e);
171
- }
172
- },
173
-
174
- async clearQueue() {
175
- const context = globalThis.getContext?.();
176
- if (!context) return;
177
- try {
178
- await api.callJsonApi("/message_queue_remove", { context });
179
- } catch (e) {
180
- console.error("Failed to clear queue:", e);
181
- }
182
- },
183
-
184
- async sendItem(itemId) {
185
- const context = globalThis.getContext?.();
186
- if (!context) return;
187
- try {
188
- await api.callJsonApi("/message_queue_send", {
189
- context,
190
- item_id: itemId,
191
- });
192
- } catch (e) {
193
- console.error("Failed to send queued message:", e);
194
- }
195
- },
196
-
197
- async sendAll() {
198
- const context = globalThis.getContext?.();
199
- if (!context || !this.hasQueue) return;
200
-
201
- // check for pending uploads and notify user
202
- if (this.pendingItems.length > 0) {
203
- await sleep(1000);
204
- if (this.pendingItems.length > 0) {
205
- toastFrontendInfo(
206
- "There are pending uploads in the queue. You can wait for them to finish or remove them.",
207
- "Pending uploads",
208
- 3,
209
- "pending-uploads",
210
- NotificationPriority.NORMAL,
211
- true,
212
- );
213
- return;
214
- }
215
- }
216
-
217
- if (!this.hasQueue) return;
218
- try {
219
- navStore.scrollToBottom();
220
- await api.callJsonApi("/message_queue_send", { context, send_all: true });
221
- } catch (e) {
222
- console.error("Failed to send all queued:", e);
223
- }
224
- },
225
-
226
- updateFromPoll() {
227
- // this.items = queue || [];
228
-
229
- if (this.pendingItems.length > 0) {
230
- const serverIds = new Set(this.items.map((i) => i.id).filter(Boolean));
231
- this.pendingItems = this.pendingItems.filter((p) => {
232
- if (!p.id) return true;
233
- return !serverIds.has(p.id);
234
- });
235
- }
236
- // this.scrollQueueToBottom();
237
- },
238
-
239
- getAttachmentUrl(filename) {
240
- return `/api/image_get?path=/a0/usr/uploads/${encodeURIComponent(filename)}`;
241
- },
242
- };
243
-
244
- const store = createStore("messageQueue", model);
245
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa736dcee1b8124b42207dd332a2c6215d54e260276bd5f42439a3baddf2b422
3
+ size 6718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/message-queue/message-queue.html CHANGED
@@ -1,245 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store } from "/components/chat/message-queue/message-queue-store.js";
5
- </script>
6
- </head>
7
- <body>
8
- <div x-data>
9
- <template x-if="$store.messageQueue?.hasQueue">
10
- <div class="queue-preview">
11
- <div class="queue-header">
12
- <span class="queue-title">
13
- Queued Messages (<span x-text="$store.messageQueue.count"></span>)
14
- </span>
15
- <div class="queue-header-actions">
16
- <button class="queue-header-btn" @click="$store.messageQueue.sendAll()" title="Send all">
17
- <x-icon name="keyboard_double_arrow_right"></x-icon>
18
- </button>
19
- <button class="queue-header-btn delete" @click="$store.messageQueue.clearQueue()" title="Clear all">
20
- <x-icon name="delete_sweep"></x-icon>
21
- </button>
22
- </div>
23
- </div>
24
- <div class="queue-items no-scrollbar">
25
- <template x-for="(item, index) in $store.messageQueue.allItems" :key="item.id">
26
- <div class="queue-item" :class="{ 'queue-item-pending': item.pending }">
27
- <div class="queue-item-content">
28
- <template x-if="item.pending">
29
- <span class="queue-item-pending-icon"></span>
30
- </template>
31
- <template x-if="!item.pending">
32
- <span class="queue-item-seq" x-text="index + 1"></span>
33
- </template>
34
- <span class="queue-item-text" x-text="item.text || '(attachment only)'"></span>
35
- <template x-if="item.attachments?.length > 0">
36
- <div class="queue-attachments">
37
- <template x-for="att in item.attachments" :key="att">
38
- <img
39
- :src="$store.messageQueue.getAttachmentUrl(att)"
40
- :alt="att"
41
- class="queue-attachment-thumb"
42
- :title="att"
43
- />
44
- </template>
45
- </div>
46
- </template>
47
- </div>
48
- <div class="queue-item-actions">
49
- <button class="queue-action-btn send" @click="$store.messageQueue.sendItem(item.id)" title="Send now">
50
- <x-icon name="send"></x-icon>
51
- </button>
52
- <button class="queue-action-btn delete" @click="$store.messageQueue.removeItem(item.id)" title="Remove">
53
- <x-icon name="close"></x-icon>
54
- </button>
55
- </div>
56
- </div>
57
- </template>
58
- </div>
59
- </div>
60
- </template>
61
- </div>
62
-
63
- <style>
64
- .queue-preview {
65
- border: 1px solid var(--color-border);
66
- border-radius: 8px;
67
- padding: var(--spacing-sm);
68
- margin-bottom: var(--spacing-xs);
69
- }
70
-
71
- .queue-header {
72
- display: flex;
73
- justify-content: space-between;
74
- align-items: center;
75
- margin-bottom: var(--spacing-xs);
76
- }
77
-
78
- .queue-title {
79
- font-size: var(--font-size-xs);
80
- color: var(--color-text-secondary);
81
- }
82
-
83
- .queue-header-actions {
84
- display: flex;
85
- gap: 2px;
86
- }
87
-
88
- .queue-header-btn {
89
- background: transparent;
90
- border: none;
91
- color: var(--color-text-secondary);
92
- cursor: pointer;
93
- padding: 2px;
94
- border-radius: 4px;
95
- display: flex;
96
- align-items: center;
97
- opacity: 0.6;
98
- transition: opacity 0.1s ease-in-out, color 0.1s ease-in-out,
99
- box-shadow 0.1s ease-in-out;
100
- }
101
-
102
- .queue-header-btn:hover {
103
- opacity: 1;
104
- box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
105
- }
106
-
107
- .queue-header-btn:active {
108
- opacity: 0.5;
109
- box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.06);
110
- }
111
-
112
- .queue-header-btn .material-symbols-outlined {
113
- font-size: var(--font-size-large);
114
- }
115
-
116
- .queue-items {
117
- display: flex;
118
- flex-direction: column;
119
- gap: 2px;
120
- max-height: 120px;
121
- overflow-y: auto;
122
- }
123
-
124
- .queue-item {
125
- display: flex;
126
- align-items: center;
127
- justify-content: space-between;
128
- gap: var(--spacing-sm);
129
- padding: 6px 8px;
130
- border-radius: 4px;
131
- cursor: default;
132
- }
133
-
134
- .queue-item:hover {
135
- background-color: var(--color-background-hover);
136
- }
137
-
138
- .queue-item-content {
139
- display: flex;
140
- align-items: center;
141
- gap: var(--spacing-sm);
142
- flex: 1;
143
- min-width: 0;
144
- }
145
-
146
- .queue-item-seq {
147
- display: inline-flex;
148
- align-items: center;
149
- justify-content: center;
150
- width: 1rem;
151
- height: 1rem;
152
- font-size: 0.5rem;
153
- color: var(--color-text-secondary);
154
- background: var(--color-background);
155
- border-radius: 50%;
156
- }
157
-
158
- .queue-item-text {
159
- font-size: var(--font-size-small);
160
- color: var(--color-text);
161
- white-space: nowrap;
162
- overflow: hidden;
163
- text-overflow: ellipsis;
164
- flex: 1;
165
- min-width: 0;
166
- }
167
-
168
- .queue-attachments {
169
- display: flex;
170
- gap: 4px;
171
- flex-shrink: 0;
172
- }
173
-
174
- .queue-attachment-thumb {
175
- width: 20px;
176
- height: 20px;
177
- object-fit: contain;
178
- border-radius: 3px;
179
- border: 1px solid var(--color-border);
180
- }
181
-
182
- .queue-item-actions {
183
- display: flex;
184
- gap: 2px;
185
- flex-shrink: 0;
186
- }
187
-
188
- .device-pointer .queue-item-actions {
189
- opacity: 0;
190
- visibility: hidden;
191
- pointer-events: none;
192
- transition: opacity 0.15s;
193
- }
194
-
195
- .device-pointer .queue-item:hover .queue-item-actions {
196
- opacity: 1;
197
- visibility: visible;
198
- pointer-events: auto;
199
- }
200
-
201
- .device-touch .queue-item-actions {
202
- opacity: 1;
203
- visibility: visible;
204
- pointer-events: auto;
205
- }
206
-
207
- .queue-action-btn {
208
- background: transparent;
209
- border: none;
210
- color: var(--color-primary);
211
- cursor: pointer;
212
- padding: 4px;
213
- border-radius: 4px;
214
- display: flex;
215
- align-items: center;
216
- }
217
-
218
- .queue-action-btn .material-symbols-outlined {
219
- font-size: var(--font-size-smaller);
220
- }
221
-
222
- .queue-item-pending {
223
- opacity: 0.6;
224
- }
225
-
226
- .queue-item-pending .queue-action-btn.send {
227
- visibility: hidden !important;
228
- pointer-events: none !important;
229
- }
230
-
231
- .queue-item-pending-icon {
232
- width: 1rem;
233
- height: 1rem;
234
- border: 2px solid var(--color-border);
235
- border-top-color: var(--color-primary);
236
- border-radius: 50%;
237
- animation: pending-spin 0.8s linear infinite;
238
- }
239
-
240
- @keyframes pending-spin {
241
- to { transform: rotate(360deg); }
242
- }
243
- </style>
244
- </body>
245
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a0fb9b33d9df338f506b05d012aa262ed37ed7d69e69366dd104ebe62c22b1c8
3
+ size 6601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/model-gate-store.js CHANGED
@@ -1,233 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import { callJsonApi } from "/js/api.js";
3
- import { store as modelConfigStore } from "/plugins/_model_config/webui/model-config-store.js";
4
- import { store as onboardingStore } from "/plugins/_onboarding/webui/onboarding-store.js";
5
- import { toastFrontendError } from "/components/notifications/notification-store.js";
6
-
7
- const ONBOARDING_MODAL_PATH = "/plugins/_onboarding/webui/onboarding.html";
8
- const STORAGE_KEY = "a0:model-gate-pending:v1";
9
- const SYNTHETIC_MESSAGE_NO_BASE = Number.MAX_SAFE_INTEGER - 2;
10
-
11
- export const store = createStore("modelGate", {
12
- active: false,
13
- connected: false,
14
- connectedLabel: "",
15
- accountConnected: false,
16
- accountLabel: "",
17
- pending: null,
18
- gateMessageId: "",
19
- choice: "",
20
- dispatching: false,
21
- _initialized: false,
22
-
23
- get introText() {
24
- if (this.connected) {
25
- return `Model connected: ${this.connectedLabel || "ready"}`;
26
- }
27
- if (this.accountConnected) {
28
- return `${this.accountLabel || "Your AI account"} is connected. Choose Main and Utility models and I'll answer right away.`;
29
- }
30
- return "I'm ready to work on this — I just need a model to think with. Pick one and I'll answer right away.";
31
- },
32
-
33
- init() {
34
- if (this._initialized) return;
35
- this._initialized = true;
36
- this.restorePending();
37
- document.addEventListener("onboarding-configured", () => {
38
- void this.dispatchPendingIfConfigured();
39
- });
40
- document.addEventListener("model-configured", () => {
41
- void this.dispatchPendingIfConfigured();
42
- });
43
- document.addEventListener("model-setup-changed", () => {
44
- void this.dispatchPendingIfConfigured();
45
- });
46
- document.addEventListener("modal-closed", () => {
47
- if (this.active && !this.connected) {
48
- this.choice = "";
49
- this.savePending();
50
- void this.dispatchPendingIfConfigured();
51
- }
52
- });
53
- },
54
-
55
- async canSendToModel() {
56
- try {
57
- const data = await callJsonApi("/plugins/_model_config/model_config_get", {});
58
- this.applyModelStatus(data);
59
- if (!data?.model_configured) await this.refreshConnectedAccountState();
60
- return !!data?.model_configured;
61
- } catch (error) {
62
- console.error("Could not check model configuration:", error);
63
- return true;
64
- }
65
- },
66
-
67
- async refreshConnectedAccountState() {
68
- try {
69
- const { store: oauthConfigStore } = await import("/plugins/_oauth/webui/oauth-config-store.js");
70
- await oauthConfigStore.loadStatus();
71
- const account = oauthConfigStore.connectedProviderCards()[0] || null;
72
- this.accountConnected = Boolean(account);
73
- this.accountLabel = account?.short_name || account?.display_name || account?.provider_id || "";
74
- return this.accountConnected;
75
- } catch (error) {
76
- console.error("Could not check connected accounts:", error);
77
- this.accountConnected = false;
78
- this.accountLabel = "";
79
- return false;
80
- }
81
- },
82
-
83
- applyModelStatus(data = {}) {
84
- modelConfigStore.modelConfigured = !!data.model_configured;
85
- modelConfigStore.modelConfiguredLabel = data.model_configured_label || "";
86
- this.connectedLabel = data.model_configured_label || this.connectedLabel || "";
87
- },
88
-
89
- start({ message, attachments, messageId, context }) {
90
- this.init();
91
- this.active = true;
92
- this.connected = false;
93
- this.pending = { message, attachments, messageId, context };
94
- this.gateMessageId = this.gateMessageId || `model-gate-${messageId}`;
95
- this.savePending();
96
- },
97
-
98
- syntheticMessages(currentContext) {
99
- this.init();
100
- if (!this.active || !this.pending || this.pending.context !== currentContext) return [];
101
- return [
102
- {
103
- no: SYNTHETIC_MESSAGE_NO_BASE,
104
- id: this.pending.messageId,
105
- type: "user",
106
- content: this.pending.message,
107
- kvps: { attachments: this.pending.attachments },
108
- },
109
- {
110
- no: SYNTHETIC_MESSAGE_NO_BASE + 1,
111
- id: this.gateMessageId,
112
- type: "model_setup_gate",
113
- },
114
- ];
115
- },
116
-
117
- mergeSyntheticMessages(logs, currentContext) {
118
- this.init();
119
- const synthetic = this.syntheticMessages(currentContext);
120
- return synthetic.length ? [...(logs || []), ...synthetic] : logs;
121
- },
122
-
123
- onCardCreate() {
124
- this.init();
125
- void this.dispatchPendingIfConfigured();
126
- },
127
-
128
- openOnboarding(choice) {
129
- this.choice = ["local", "account"].includes(choice) ? choice : "cloud";
130
- this.savePending();
131
- onboardingStore.presetMode = this.choice;
132
- const modalPromise = window.openModal?.(ONBOARDING_MODAL_PATH);
133
- void Promise.resolve(modalPromise).then(() => this.dispatchPendingIfConfigured());
134
- },
135
-
136
- async openAdvancedModelConfiguration() {
137
- try {
138
- const data = await callJsonApi("/plugins/_model_config/model_config_get", {});
139
- await modelConfigStore.openPresetEditor(
140
- data?.selected_preset || data?.configured_preset || "Default"
141
- );
142
- await this.dispatchPendingIfConfigured();
143
- } catch (error) {
144
- console.error("Could not open model preset editor:", error);
145
- void toastFrontendError(
146
- error?.message || "Could not open model presets.",
147
- "Advanced model configuration"
148
- );
149
- }
150
- },
151
-
152
- async dispatchPendingIfConfigured() {
153
- this.init();
154
- if (this.dispatching || !this.pending) return;
155
- const configured = await this.canSendToModel();
156
- if (!configured) return;
157
-
158
- const pending = this.pending;
159
- this.pending = null;
160
- this.connected = true;
161
- this.dispatching = true;
162
- this.clearSavedPending();
163
- try {
164
- await globalThis.sendMessage?.({
165
- bypassModelGate: true,
166
- skipExtensions: true,
167
- preserveInput: true,
168
- message: pending.message,
169
- attachments: pending.attachments,
170
- messageId: pending.messageId,
171
- context: pending.context,
172
- });
173
- } finally {
174
- this.dispatching = false;
175
- }
176
- },
177
-
178
- savePending() {
179
- const message = String(this.pending?.message || "").trim();
180
- const context = String(this.pending?.context || "");
181
- const messageId = String(this.pending?.messageId || "");
182
- const attachments = Array.isArray(this.pending?.attachments) ? this.pending.attachments : [];
183
- if (!message || !context || !messageId || attachments.length) {
184
- this.clearSavedPending();
185
- return;
186
- }
187
-
188
- try {
189
- sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
190
- pending: { message, attachments: [], messageId, context },
191
- gateMessageId: this.gateMessageId || `model-gate-${messageId}`,
192
- choice: this.choice,
193
- }));
194
- } catch (_error) {
195
- // Session storage can be unavailable in private or locked-down browser modes.
196
- }
197
- },
198
-
199
- restorePending() {
200
- if (this.pending) return;
201
-
202
- let saved = null;
203
- try {
204
- saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
205
- } catch (_error) {
206
- this.clearSavedPending();
207
- return;
208
- }
209
-
210
- const message = String(saved?.pending?.message || "").trim();
211
- const context = String(saved?.pending?.context || "");
212
- const messageId = String(saved?.pending?.messageId || "");
213
- if (!message || !context || !messageId) {
214
- this.clearSavedPending();
215
- return;
216
- }
217
-
218
- this.active = true;
219
- this.connected = false;
220
- this.pending = { message, attachments: [], messageId, context };
221
- this.gateMessageId = String(saved?.gateMessageId || `model-gate-${messageId}`);
222
- this.choice = ["local", "cloud", "account"].includes(saved?.choice) ? saved.choice : "";
223
- },
224
-
225
- clearSavedPending() {
226
- try {
227
- sessionStorage.removeItem(STORAGE_KEY);
228
- } catch (_error) {
229
- // Ignore unavailable storage; the in-memory gate state is still authoritative.
230
- }
231
- },
232
-
233
- });
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:508d0f27865b8aa46fcb8c99788caba98a1b5258d6f9c8569a3e46a75eb8c53d
3
+ size 7685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/model-setup-gate.html CHANGED
@@ -1,203 +1,3 @@
1
- <html>
2
- <head>
3
- <script type="module">
4
- import { store as modelGateStore } from "/components/chat/model-gate-store.js";
5
- </script>
6
- </head>
7
- <body>
8
- <div x-data>
9
- <template x-if="$store.modelGate">
10
- <section class="model-gate" x-create="$store.modelGate.onCardCreate()">
11
- <template x-if="$store.modelGate.connected">
12
- <div class="model-gate-chip">
13
- <x-icon aria-hidden="true" name="check_circle"></x-icon>
14
- <span x-text="$store.modelGate.introText"></span>
15
- </div>
16
- </template>
17
-
18
- <template x-if="!$store.modelGate.connected">
19
- <div class="model-gate-body">
20
- <p class="model-gate-intro" x-text="$store.modelGate.introText"></p>
21
-
22
- <div class="model-gate-card">
23
- <div class="model-gate-fork is-single" x-show="$store.modelGate.accountConnected">
24
- <button type="button"
25
- class="model-gate-option"
26
- @click="$store.modelGate.openAdvancedModelConfiguration()">
27
- <span class="model-gate-option-title">
28
- <x-icon aria-hidden="true" name="tune"></x-icon>
29
- <span>Choose models</span>
30
- </span>
31
- <span class="model-gate-option-sub">Select Main and Utility models</span>
32
- </button>
33
- </div>
34
-
35
- <div class="model-gate-fork" x-show="!$store.modelGate.accountConnected">
36
- <button type="button"
37
- class="model-gate-option"
38
- :class="{ selected: $store.modelGate.choice === 'cloud' }"
39
- @click="$store.modelGate.openOnboarding('cloud')">
40
- <span class="model-gate-option-title">
41
- <x-icon aria-hidden="true" name="cloud"></x-icon>
42
- <span>Cloud provider</span>
43
- </span>
44
- <span class="model-gate-option-sub">Connect with an API key</span>
45
- </button>
46
- <button type="button"
47
- class="model-gate-option"
48
- :class="{ selected: $store.modelGate.choice === 'account' }"
49
- @click="$store.modelGate.openOnboarding('account')">
50
- <span class="model-gate-option-title">
51
- <x-icon aria-hidden="true" name="account_circle"></x-icon>
52
- <span>AI account</span>
53
- </span>
54
- <span class="model-gate-option-sub">Codex, Copilot, Gemini, Grok</span>
55
- </button>
56
- <button type="button"
57
- class="model-gate-option"
58
- :class="{ selected: $store.modelGate.choice === 'local' }"
59
- @click="$store.modelGate.openOnboarding('local')">
60
- <span class="model-gate-option-title">
61
- <x-icon aria-hidden="true" name="memory"></x-icon>
62
- <span>Local model</span>
63
- </span>
64
- <span class="model-gate-option-sub">Runs on your machine</span>
65
- </button>
66
- </div>
67
- </div>
68
-
69
- <p class="model-gate-footnote">
70
- <span>Your message sends automatically once a model is connected.</span>
71
- <button type="button" @click="$store.modelGate.openAdvancedModelConfiguration()">Advanced model configuration</button>
72
- </p>
73
- </div>
74
- </template>
75
- </section>
76
- </template>
77
- </div>
78
-
79
- <style>
80
- .model-gate {
81
- width: min(100%, 36rem);
82
- color: var(--color-text);
83
- font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
84
- }
85
-
86
- .model-gate-body {
87
- display: grid;
88
- gap: 0.65rem;
89
- container-type: inline-size;
90
- }
91
-
92
- .model-gate-intro {
93
- margin: 0;
94
- color: color-mix(in srgb, var(--color-text) 84%, transparent);
95
- line-height: 1.45;
96
- }
97
-
98
- .model-gate-card {
99
- display: grid;
100
- gap: 0.75rem;
101
- }
102
-
103
- .model-gate-fork {
104
- display: flex;
105
- gap: 0.6rem;
106
- }
107
-
108
- .model-gate-fork .model-gate-option {
109
- flex: 1 1 0;
110
- min-width: 0;
111
- }
112
-
113
- @container (max-width: 30rem) {
114
- .model-gate-fork {
115
- flex-direction: column;
116
- }
117
- }
118
-
119
- .model-gate-option {
120
- border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
121
- border-radius: 8px;
122
- background: color-mix(in srgb, var(--color-background) 52%, transparent);
123
- color: inherit;
124
- font: inherit;
125
- text-align: left;
126
- cursor: pointer;
127
- }
128
-
129
- .model-gate-option {
130
- display: grid;
131
- gap: 0.25rem;
132
- padding: 0.72rem;
133
- }
134
-
135
- .model-gate-option:hover,
136
- .model-gate-option:focus-visible,
137
- .model-gate-option.selected {
138
- border-color: color-mix(in srgb, var(--color-border) 86%, var(--color-text) 14%);
139
- background: color-mix(in srgb, var(--color-background-hover) 60%, var(--color-panel));
140
- outline: none;
141
- }
142
-
143
- .model-gate-option-title {
144
- display: inline-flex;
145
- align-items: center;
146
- gap: 0.4rem;
147
- font-weight: 560;
148
- }
149
-
150
- .model-gate-option-title .material-symbols-outlined {
151
- font-size: 1.15rem;
152
- color: color-mix(in srgb, var(--color-text) 72%, transparent);
153
- }
154
-
155
- .model-gate-option-sub,
156
- .model-gate-footnote {
157
- color: color-mix(in srgb, var(--color-text) 58%, transparent);
158
- font-size: 0.78rem;
159
- line-height: 1.35;
160
- }
161
-
162
- .model-gate-footnote {
163
- display: flex;
164
- align-items: center;
165
- justify-content: space-between;
166
- gap: 0.7rem;
167
- flex-wrap: wrap;
168
- }
169
-
170
- .model-gate-footnote button {
171
- border: 0;
172
- background: transparent;
173
- color: color-mix(in srgb, #6f8cff 82%, var(--color-text));
174
- font: inherit;
175
- font-size: 0.8rem;
176
- cursor: pointer;
177
- padding: 0;
178
- }
179
-
180
- .model-gate-chip {
181
- display: inline-flex;
182
- align-items: center;
183
- gap: 0.4rem;
184
- border: 1px solid color-mix(in srgb, #7bc995 44%, var(--color-border));
185
- border-radius: 999px;
186
- padding: 0.35rem 0.65rem;
187
- color: color-mix(in srgb, var(--color-text) 88%, #7bc995);
188
- background: color-mix(in srgb, #7bc995 10%, var(--color-panel));
189
- font-size: 0.84rem;
190
- }
191
-
192
- .model-gate-chip .material-symbols-outlined {
193
- font-size: 1rem;
194
- }
195
-
196
- @media (max-width: 480px) {
197
- .model-gate-fork {
198
- flex-direction: column;
199
- }
200
- }
201
- </style>
202
- </body>
203
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:84203c0efe43657c9506ab678938f133cfd2d2738363cf77449da3a106558c61
3
+ size 6522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/navigation/chat-navigation-store.js CHANGED
@@ -1,127 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import {
3
- getMessageWindowState,
4
- loadAdjacentMessageWindow,
5
- scrollMessageWindowToEdge,
6
- } from "/js/messages.js";
7
-
8
- const model = {
9
- // Configuration
10
- scrollMargin: 60,
11
- prevTolerance: 35,
12
- nextTolerance: 5,
13
-
14
- init() {
15
- // Any initialization if needed
16
- },
17
-
18
- async scrollToTop() {
19
- await scrollMessageWindowToEdge("start");
20
- },
21
-
22
- async scrollToBottom() {
23
- await scrollMessageWindowToEdge("end");
24
- },
25
-
26
- async scrollToPrevUserMessage() {
27
- const scroller = this._getChatHistoryEl();
28
- if (!scroller) return;
29
-
30
- const positions = this._getUserMessagePositions(scroller);
31
- const scrollerRect = scroller.getBoundingClientRect();
32
-
33
- const prevThreshold = this.scrollMargin - this.prevTolerance; // 25px
34
-
35
- const currentIndex = positions.findIndex((p) => {
36
- const relativeTop = p.el.getBoundingClientRect().top - scrollerRect.top;
37
- return relativeTop >= prevThreshold;
38
- });
39
-
40
- if (currentIndex > 0) {
41
- // Go to previous message
42
- positions[currentIndex - 1].el.scrollIntoView({ block: "start", behavior: "smooth" });
43
- } else if (currentIndex === 0) {
44
- if (getMessageWindowState().hasOlder) {
45
- await loadAdjacentMessageWindow("older");
46
- this._scrollToLastUserAboveThreshold(scroller, prevThreshold);
47
- } else {
48
- scroller.scrollTo({ top: 0, behavior: "instant" });
49
- }
50
- } else if (currentIndex === -1 && positions.length > 0) {
51
- // All messages are above the threshold (scrolled past), scroll to bottom
52
- positions[positions.length - 1].el.scrollIntoView({ block: "start", behavior: "smooth" });
53
- } else if (positions.length === 0 && getMessageWindowState().hasOlder) {
54
- await loadAdjacentMessageWindow("older");
55
- this._scrollToLastUserAboveThreshold(scroller, prevThreshold);
56
- }
57
- },
58
-
59
- async scrollToNextUserMessage() {
60
- const scroller = this._getChatHistoryEl();
61
- if (!scroller) return;
62
-
63
- const positions = this._getUserMessagePositions(scroller);
64
- const scrollerRect = scroller.getBoundingClientRect();
65
-
66
- const nextThreshold = this.scrollMargin + this.nextTolerance; // 65px
67
-
68
- // Find first message below the threshold
69
- const targetIndex = positions.findIndex((p) => {
70
- const relativeTop = p.el.getBoundingClientRect().top - scrollerRect.top;
71
- return relativeTop > nextThreshold;
72
- });
73
-
74
- if (targetIndex !== -1) {
75
- // Go to that message
76
- positions[targetIndex].el.scrollIntoView({ block: "start", behavior: "smooth" });
77
- } else {
78
- if (getMessageWindowState().hasNewer) {
79
- await loadAdjacentMessageWindow("newer");
80
- this._scrollToFirstUserBelowThreshold(scroller, nextThreshold);
81
- } else {
82
- await this.scrollToBottom();
83
- }
84
- }
85
- },
86
-
87
- _scrollToLastUserAboveThreshold(scroller, threshold) {
88
- const scrollerRect = scroller.getBoundingClientRect();
89
- const positions = this._getUserMessagePositions(scroller);
90
- const candidates = positions.filter(
91
- ({ el }) => el.getBoundingClientRect().top - scrollerRect.top < threshold
92
- );
93
- candidates.at(-1)?.el.scrollIntoView({ block: "start", behavior: "smooth" });
94
- },
95
-
96
- _scrollToFirstUserBelowThreshold(scroller, threshold) {
97
- const scrollerRect = scroller.getBoundingClientRect();
98
- const target = this._getUserMessagePositions(scroller).find(
99
- ({ el }) => el.getBoundingClientRect().top - scrollerRect.top > threshold
100
- );
101
- target?.el.scrollIntoView({ block: "start", behavior: "smooth" });
102
- },
103
-
104
- // Helpers
105
- _getChatHistoryEl() {
106
- return document.getElementById("chat-history");
107
- },
108
-
109
- _getUserMessagePositions(scroller) {
110
- const userMessageEls = Array.from(
111
- scroller.querySelectorAll(".message-container.user-container")
112
- );
113
- // Helper for getElementTopInScroller
114
- const getTop = (el) => {
115
- const elRect = el.getBoundingClientRect();
116
- const scrollerRect = scroller.getBoundingClientRect();
117
- return elRect.top - scrollerRect.top + scroller.scrollTop;
118
- };
119
-
120
- return userMessageEls
121
- .map((el) => ({ el, top: getTop(el) }))
122
- .sort((a, b) => a.top - b.top);
123
- }
124
- };
125
-
126
- const store = createStore("chatNavigation", model);
127
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:319bdd51040b0b65ab5219f34d951556736c799c5755b942ff0f527707d18046
3
+ size 4661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/chat/top-section/chat-top-store.js CHANGED
@@ -1,13 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
-
3
- // define the model object holding data and functions
4
- const model = {
5
- connected: false,
6
- progressActive: false, // true when progress bar is active
7
- };
8
-
9
- // convert it to alpine store
10
- const store = createStore("chatTop", model);
11
-
12
- // export for use in other files
13
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b72133213443c983195c6cd378516d26c74835719dec7b8e2a16603a9a514b1b
3
+ size 334
 
 
 
 
 
 
 
 
 
 
webui/components/chat/top-section/chat-top.html CHANGED
@@ -1,42 +1,3 @@
1
- <html>
2
-
3
- <head>
4
- <title>Example component or modal</title>
5
-
6
- <!-- Import the alpine store -->
7
- <script type="module">
8
- import { store } from "/components/chat/top-section/chat-top-store.js";
9
- import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
10
- </script>
11
- </head>
12
-
13
- <body>
14
-
15
- <!-- This construct of x-data + x-if is used to ensure the component is only rendered when the store is available -->
16
- <div x-data>
17
- <template x-if="$store.chatTop">
18
- <!-- Time and Date -->
19
- <div id="time-date-container">
20
- <x-extension id="chat-top-start"></x-extension>
21
- <div id="time-date" x-show="$store.preferences.isUiControlVisible('time')"></div>
22
- <div x-show="$store.preferences.isUiControlVisible('connectionStatus')">
23
- <x-component path="sync/sync-status.html"></x-component>
24
- </div>
25
- <!-- Notification Toggle positioned next to time-date -->
26
- <x-component path="notifications/notification-icons.html"></x-component>
27
- <!-- Project Selector -->
28
- <div x-show="$store.preferences.isUiControlVisible('projectSelector')">
29
- <x-component path="projects/project-selector.html"></x-component>
30
- </div>
31
- <x-extension id="chat-top-end"></x-extension>
32
- </div>
33
- </template>
34
- </div>
35
-
36
- <style>
37
- #time-date-container > x-extension { display: contents; }
38
- </style>
39
-
40
- </body>
41
-
42
- </html>
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:80a1150109c8e4c7fbb4a5be3d759536a6c1bb813af1edae4c13ef1945fc7074
3
+ size 1579
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/dropdown/AGENTS.md CHANGED
@@ -1,26 +1,3 @@
1
- # Dropdown Component DOX
2
-
3
- ## Purpose
4
-
5
- - Own the shared dropdown component used by WebUI controls.
6
-
7
- ## Ownership
8
-
9
- - `dropdown.html` owns dropdown markup, behavior hooks, and component-local styling.
10
-
11
- ## Local Contracts
12
-
13
- - Keep keyboard, focus, and click-outside behavior compatible with existing callers.
14
- - Avoid caller-specific assumptions in the shared dropdown.
15
-
16
- ## Work Guidance
17
-
18
- - Prefer component attributes and slots over hardcoded option sets.
19
-
20
- ## Verification
21
-
22
- - Smoke-test each caller that uses the dropdown after behavior changes.
23
-
24
- ## Child DOX Index
25
-
26
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f3838921b0e98ec0b562a2a7d32f5e9f5b962cd6f4e4ac9b6cd9ac2e9243caf
3
+ size 582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/dropdown/dropdown.html CHANGED
@@ -1,69 +1,3 @@
1
- <!--
2
- Reusable Dropdown Component
3
-
4
- Usage:
5
- <x-component data-component="ui/dropdown" data-props='{"id": "my-dropdown"}'>
6
- <template x-slot:trigger>
7
- <button>Click me</button>
8
- </template>
9
- <template x-slot:menu>
10
- <button class="dropdown-item" @click="...">Item 1</button>
11
- <div class="dropdown-separator"></div>
12
- <button class="dropdown-item" @click="...">Item 2</button>
13
- </template>
14
- </x-component>
15
-
16
- Or use the simpler inline approach with x-data:
17
- <div class="dropdown" x-data="{ open: false }" @click.outside="open = false">
18
- <button class="dropdown-trigger" @click="open = !open">...</button>
19
- <div class="dropdown-menu" x-show="open" x-transition>...</div>
20
- </div>
21
- -->
22
- <html>
23
- <head>
24
- <!-- Dropdown uses global styles from index.css -->
25
- </head>
26
- <body>
27
- <div
28
- class="dropdown"
29
- x-data="{
30
- open: false,
31
- toggle() { this.open = !this.open; },
32
- close() { this.open = false; }
33
- }"
34
- @click.outside="close()"
35
- @keydown.escape.window="close()"
36
- >
37
- <!-- Trigger slot -->
38
- <div class="dropdown-trigger-wrapper" @click="toggle()">
39
- <slot name="trigger">
40
- <button class="dropdown-trigger" type="button">
41
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
42
- <path d="M6 9l6 6 6-6"/>
43
- </svg>
44
- </button>
45
- </slot>
46
- </div>
47
-
48
- <!-- Menu slot -->
49
- <div
50
- class="dropdown-menu"
51
- x-show="open"
52
- @click="close()"
53
- >
54
- <slot name="menu">
55
- <!-- Default empty menu -->
56
- </slot>
57
- </div>
58
- </div>
59
-
60
- <style>
61
- /* Component-specific styles (layout helpers) */
62
- .dropdown-trigger-wrapper {
63
- display: contents;
64
- }
65
-
66
- </style>
67
- </body>
68
- </html>
69
-
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3a1b08991339508618332fc2909fd59fe3c6ea8958c1e46103f8303eebbf0ed
3
+ size 1810
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/messages/AGENTS.md CHANGED
@@ -1,38 +1,3 @@
1
- # Message Components DOX
2
-
3
- ## Purpose
4
-
5
- - Own message rendering helpers, action buttons, process groups, and resize behavior.
6
-
7
- ## Ownership
8
-
9
- - `action-buttons/` owns simple message action controls.
10
- - `process-group/` owns grouped process-step DOM and styling helpers.
11
- - `resize/` owns message resize state.
12
-
13
- ## Local Contracts
14
-
15
- - Keep message DOM helpers compatible with extension points that modify rendered messages.
16
- - Sanitize or safely render model/user-provided content through shared rendering paths.
17
- - Avoid layout shifts that break long-running message streaming.
18
- - Keep message action chrome out of text selection so copy/paste captures message content without button labels or icons.
19
- - Reconcile standard action buttons in place during streamed updates so hover, focus, tooltips, and click feedback survive while handlers receive the latest message data; preserve extension-owned buttons in the same action bar.
20
- - Order standard message actions as Detail, Copy, then Speak; omit unavailable actions without changing the relative order of the remaining controls. Plugin-rendered message actions must follow the same order.
21
- - Keep collapsed process-step detail text out of the DOM; opening a step may materialize its current cached log data and collapsing it must discard that heavy detail again without removing extension action hooks.
22
- - Preference-driven process detail modes must await the same materialization path as manual expansion and accept an explicit chat-history target for off-screen window staging. `STEP` opens only the current non-utility step at the live tail; historical windows must not invent a current step at their boundary.
23
- - Keep oversized standalone replay bodies and key/value tables in a bounded preview state until the user expands them; collapsing must remove the full body again.
24
- - Message-window boundaries must not split process groups. Groups with more than 50 steps initially render their newest 50 steps and prepend earlier steps in 50-step increments through the group-local `Show more` control while retaining stable full-group header metrics.
25
- - The process-group `Show more` paging control uses the same understated, non-underlined typography and hover-opacity treatment as message-body expansion controls.
26
- - A root response may attach only to a substantive process render unit. Utility-prefixed units remain visible even while utility steps are hidden; standalone utility-only groups remain separate and hidden while utility messages are disabled, and completed groups must not absorb later utility records. Determine this from full-log render metadata, not partially mounted DOM children.
27
-
28
- ## Work Guidance
29
-
30
- - Coordinate changes with `webui/js/messages.js` and frontend extension hooks.
31
-
32
- ## Verification
33
-
34
- - Smoke-test message rendering, action buttons, process groups, and resizing after changes.
35
-
36
- ## Child DOX Index
37
-
38
- No child DOX files.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0c8e79e3f5634742fab1915654221fd49f925c9ea29d9ae79590dd1dc9543722
3
+ size 2884
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/messages/action-buttons/simple-action-buttons.css CHANGED
@@ -1,153 +1,3 @@
1
- /* ===========================================
2
- Action Buttons - Icon-only copy/speak/view details
3
- Show on hover for pointer devices, always visible on touch
4
- =========================================== */
5
-
6
- .step-action-buttons {
7
- display: flex;
8
- flex-direction: row;
9
- align-items: center;
10
- gap: var(--spacing-sm);
11
- padding: var(--spacing-sm) 0;
12
- opacity: 0;
13
- transition: opacity 0.2s ease-in-out;
14
- pointer-events: none;
15
- user-select: none;
16
- }
17
-
18
- .step-action-buttons .expand-btn {
19
- display: none;
20
- align-items: center;
21
- background: transparent;
22
- border: none;
23
- color: var(--color-text-muted);
24
- font-family: "Rubik", Arial, Helvetica, sans-serif;
25
- font-size: var(--font-size-xs);
26
- cursor: pointer;
27
- padding: var(--spacing-xs) 0;
28
- opacity: 0.7;
29
- transition: opacity 0.15s ease;
30
- }
31
-
32
- .step-action-buttons .expand-btn:hover {
33
- opacity: 1;
34
- }
35
-
36
- .step-action-buttons .action-button,
37
- .action-button {
38
- display: flex;
39
- align-items: center;
40
- justify-content: center;
41
- background: transparent;
42
- border: none;
43
- padding: 2px;
44
- cursor: pointer;
45
- }
46
-
47
- .step-action-buttons .action-button .material-symbols-outlined,
48
- .action-button .material-symbols-outlined {
49
- font-size: 0.85rem;
50
- line-height: 1;
51
- color: var(--color-message-text);
52
- font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
53
- transition: color 0.15s ease;
54
- }
55
-
56
- .step-action-buttons .action-button:hover .material-symbols-outlined,
57
- .action-button:hover .material-symbols-outlined {
58
- color: var(--color-text);
59
- }
60
-
61
- /* Success state */
62
- .step-action-buttons .action-button.success .material-symbols-outlined,
63
- .action-button.success .material-symbols-outlined {
64
- color: #4CAF50;
65
- font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
66
- }
67
-
68
- /* Error state */
69
- .step-action-buttons .action-button.error .material-symbols-outlined,
70
- .action-button.error .material-symbols-outlined {
71
- color: var(--color-accent);
72
- font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
73
- }
74
-
75
- .show-more-btn,
76
- .show-less-btn {
77
- display: none;
78
- }
79
-
80
- .message.message-collapsible.has-overflow:not(.expanded) > .step-action-buttons {
81
- opacity: 1;
82
- pointer-events: auto;
83
- }
84
-
85
- .message.message-collapsible.has-overflow:not(.expanded)
86
- > .step-action-buttons
87
- .show-more-btn {
88
- display: inline-flex;
89
- pointer-events: auto;
90
- }
91
-
92
- .device-pointer
93
- .message.message-collapsible.has-overflow:not(.expanded)
94
- > .step-action-buttons
95
- .action-button {
96
- opacity: 0;
97
- pointer-events: none;
98
- }
99
-
100
- .device-pointer
101
- .message.message-collapsible.has-overflow:not(.expanded):hover
102
- > .step-action-buttons
103
- .action-button {
104
- opacity: 1;
105
- pointer-events: auto;
106
- }
107
-
108
- .device-pointer
109
- .message.message-collapsible.has-overflow.expanded:hover
110
- > .step-action-buttons
111
- .show-less-btn {
112
- display: inline-flex;
113
- pointer-events: auto;
114
- }
115
-
116
- .device-touch
117
- .message.message-collapsible.has-overflow.expanded
118
- > .step-action-buttons
119
- .show-less-btn {
120
- display: inline-flex;
121
- pointer-events: auto;
122
- }
123
-
124
- /* User messages: right-aligned */
125
- .message-user .step-action-buttons {
126
- justify-content: flex-end;
127
- width: 100%;
128
- }
129
-
130
- .message-user .step-action-buttons .expand-btn {
131
- order: 1;
132
- }
133
-
134
-
135
-
136
- /* ===========================================
137
- Hover behavior - Pointer devices
138
- =========================================== */
139
-
140
- .device-pointer .step-action-buttons:hover,
141
- .device-pointer :hover > .step-action-buttons {
142
- opacity: 1;
143
- pointer-events: auto;
144
- }
145
-
146
- /* ===========================================
147
- Touch devices - Always visible
148
- =========================================== */
149
-
150
- .device-touch .step-action-buttons {
151
- opacity: 1;
152
- pointer-events: auto;
153
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba3db6841e04158cf6798cfebc7bbc878cd885a9935ba6e64938371327ef8f3c
3
+ size 3627
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/messages/action-buttons/simple-action-buttons.js CHANGED
@@ -1,134 +1,3 @@
1
- // Message Action Buttons - DOM helpers for message action buttons
2
-
3
- import { ICON_SELECTOR, setIconName } from "/js/icons.js";
4
-
5
- const ACTION_ICON_MAP = {
6
- detail: "open_in_full",
7
- speak: "volume_up",
8
- copy: "content_copy",
9
- };
10
-
11
- const ACTION_LABELS = {
12
- detail: "View details",
13
- speak: "Speak",
14
- copy: "Copy",
15
- };
16
-
17
- function resolveActionIcon(icon) {
18
- if (!icon) return "";
19
- return ACTION_ICON_MAP[icon] || icon;
20
- }
21
-
22
- function buildActionLabel(icon, text) {
23
- const baseLabel = ACTION_LABELS[icon] || text || icon;
24
- if (text && ACTION_LABELS[icon]) return `${ACTION_LABELS[icon]} ${text}`;
25
- return baseLabel;
26
- }
27
-
28
- /**
29
- * Copy text to clipboard with fallback for non-secure contexts
30
- */
31
- export async function copyToClipboard(text) {
32
- if (navigator.clipboard && window.isSecureContext) {
33
- await navigator.clipboard.writeText(text);
34
- } else {
35
- // Fallback for local dev / non-secure contexts
36
- const textarea = document.createElement("textarea");
37
- textarea.value = text;
38
- textarea.style.cssText = "position:fixed;left:-9999px";
39
- document.body.appendChild(textarea);
40
- textarea.select();
41
- document.execCommand("copy");
42
- document.body.removeChild(textarea);
43
- }
44
- }
45
-
46
- /**
47
- * Show visual feedback on a button (success/error state)
48
- */
49
- export function showButtonFeedback(button, success, originalIcon) {
50
- const icon = button.querySelector(ICON_SELECTOR);
51
- if (!icon) return;
52
-
53
- setIconName(icon, success ? "check" : "error");
54
- button.classList.add(success ? "success" : "error");
55
-
56
- setTimeout(() => {
57
- setIconName(icon, originalIcon);
58
- button.classList.remove("success", "error");
59
- }, 1000);
60
- }
61
-
62
- export function syncActionButtons(container, actionButtons = []) {
63
- const previous = container.__managedActionButtons || [];
64
- const next = actionButtons.filter(Boolean).map((button, index) => {
65
- const existing = previous[index];
66
- if (
67
- existing?.isConnected &&
68
- existing.dataset.actionKey === button.dataset.actionKey
69
- ) {
70
- existing.__actionHandler = button.__actionHandler;
71
- return existing;
72
- }
73
- existing?.remove();
74
- return button;
75
- });
76
- const anchor = [...container.children].find(
77
- (child) =>
78
- !previous.includes(child) && !child.classList.contains("expand-btn"),
79
- );
80
-
81
- previous.slice(next.length).forEach((button) => button.remove());
82
- next.forEach((button) => container.insertBefore(button, anchor || null));
83
- container.__managedActionButtons = next;
84
- }
85
-
86
- /**
87
- * Create action button element
88
- *
89
- * @param {string} icon
90
- * @param {string} [text]
91
- * @param {(() => (any | Promise<any>)) | null} [handler]
92
- * @returns {HTMLButtonElement}
93
- */
94
- export function createActionButton(icon, text = "", handler = null) {
95
- const iconName = resolveActionIcon(icon);
96
-
97
- const button = document.createElement("button");
98
- button.type = "button";
99
- button.className = `action-button action-${icon}`;
100
- button.dataset.actionKey = `${icon}:${text}`;
101
- button.__actionHandler = handler;
102
- const label = buildActionLabel(icon, text);
103
- if (label) {
104
- button.setAttribute("aria-label", label);
105
- button.setAttribute("title", label);
106
- }
107
-
108
- if (iconName) {
109
- const iconElement = document.createElement("x-icon");
110
- iconElement.name = iconName;
111
- button.appendChild(iconElement);
112
- } else if (text) {
113
- button.textContent = text;
114
- }
115
-
116
- button.addEventListener("click", async (event) => {
117
- event.stopPropagation();
118
- if (typeof button.__actionHandler !== "function") return;
119
- const shouldShowFeedback = Boolean(iconName); // icon === "copy" || icon === "speak";
120
- try {
121
- await button.__actionHandler();
122
- if (shouldShowFeedback) {
123
- showButtonFeedback(button, true, iconName);
124
- }
125
- } catch (err) {
126
- console.error("Action button failed:", err);
127
- if (shouldShowFeedback) {
128
- showButtonFeedback(button, false, iconName);
129
- }
130
- }
131
- });
132
-
133
- return button;
134
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:906e3cf12b86ff41aa8ee6c3059a1dffb98c258f3765c7d41e42eb1f448a9778
3
+ size 3881
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/messages/process-group/process-group-dom.js CHANGED
@@ -1,76 +1,3 @@
1
- /**
2
- * Process group DOM utilities (no store/state)
3
- */
4
- import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
5
-
6
- export async function applyModeSteps(
7
- detailMode,
8
- showUtils,
9
- chatHistory = document.getElementById("chat-history"),
10
- ) {
11
- const mode =
12
- detailMode ||
13
- preferencesStore.detailMode ||
14
- "current";
15
-
16
- if (!chatHistory) return;
17
-
18
- chatHistory.dataset.detailMode = mode;
19
-
20
- const shouldExpand = mode !== "collapsed";
21
- const allMode = mode === "expanded";
22
- const showUtilsFlag =
23
- typeof showUtils === "boolean"
24
- ? showUtils
25
- : preferencesStore.showUtils || false;
26
- const pending = [];
27
- const messages = Array.from(chatHistory.querySelectorAll(".process-group"));
28
- const windowEnd = Number(chatHistory.dataset.messageWindowEnd);
29
- const windowTotal = Number(chatHistory.dataset.messageWindowTotal);
30
- const isAtTail =
31
- Number.isFinite(windowEnd) &&
32
- Number.isFinite(windowTotal) &&
33
- windowEnd >= windowTotal;
34
- for (let i = 0; i < messages.length; i += 1) {
35
- const group = messages[i];
36
- if (typeof group.__setExpanded === "function") {
37
- pending.push(Promise.resolve(group.__setExpanded(shouldExpand)));
38
- } else {
39
- group.classList.toggle("expanded", shouldExpand);
40
- }
41
-
42
- const steps = Array.from(group.querySelectorAll(".process-step"));
43
- const isComplete =
44
- group.hasAttribute("data-group-complete") ||
45
- Boolean(group.querySelector(".process-group-response"));
46
- let currentStep = null;
47
- if (
48
- mode === "current" &&
49
- isAtTail &&
50
- group === messages[messages.length - 1] &&
51
- !isComplete
52
- ) {
53
- for (let si = steps.length - 1; si >= 0; si -= 1) {
54
- if (
55
- showUtilsFlag ||
56
- !steps[si].classList.contains("message-util")
57
- ) {
58
- currentStep = steps[si];
59
- break;
60
- }
61
- }
62
- }
63
-
64
- for (let si = 0; si < steps.length; si += 1) {
65
- const step = steps[si];
66
- const shouldExpandStep = allMode || step === currentStep;
67
- if (typeof step.__setExpanded === "function") {
68
- pending.push(Promise.resolve(step.__setExpanded(shouldExpandStep)));
69
- } else {
70
- step.classList.toggle("expanded", shouldExpandStep);
71
- }
72
- }
73
- }
74
-
75
- await Promise.allSettled(pending);
76
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ef180e4cc6f6feb0498139015cb64ce7ae4fbf09083d3be92dc8ebb3e9a8e4f9
3
+ size 2305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/messages/process-group/process-group.css CHANGED
@@ -1,914 +1,3 @@
1
- .process-group {
2
- display: inline-flex;
3
- flex-direction: column;
4
- position: relative;
5
- z-index: 1;
6
- margin: var(--spacing-xs) 0;
7
- padding: var(--spacing-xs) 0;
8
- min-width: 200px;
9
- max-width: 100%;
10
- box-sizing: border-box;
11
- flex-shrink: 0;
12
- }
13
-
14
- .process-group.utility-only {
15
- display: none !important;
16
- }
17
-
18
- .show-utility-messages .process-group.utility-only {
19
- display: flex !important;
20
- }
21
-
22
- /* Embedded Process Group inside Response */
23
- .process-group.embedded {
24
- display: flex;
25
- flex-direction: column;
26
- width: 100%;
27
- margin: 0;
28
- border-radius: var(--border-radius) var(--border-radius) 0 0;
29
- /* border-bottom: 1px solid rgba(255, 255, 255, 0.08); */
30
- background: transparent;
31
- }
32
-
33
- /* Message container with embedded process group */
34
- .message-container.has-process-group {
35
- display: inline-flex;
36
- flex-direction: column;
37
- padding: 0;
38
- overflow: hidden;
39
- min-width: 200px;
40
- max-width: 100%;
41
- }
42
-
43
- .message-container.has-process-group > .message {
44
- border: none;
45
- background: transparent;
46
- margin: 0;
47
- }
48
-
49
- /* Process Group Header */
50
- .process-group-header {
51
- display: flex;
52
- align-items: center;
53
- padding: 0;
54
- cursor: pointer;
55
- user-select: none;
56
- transition: opacity 0.15s ease;
57
- min-height: 22px;
58
- gap: 6px;
59
- white-space: nowrap;
60
- }
61
-
62
- .process-group-header:hover {
63
- opacity: 0.85;
64
- }
65
-
66
- /* Expand/collapse triangle - CSS-only (no Material Icons) */
67
- .process-group-header .expand-icon {
68
- display: inline-block;
69
- width: 0;
70
- height: 0;
71
- border-style: solid;
72
- border-width: 4px 0 4px 6px;
73
- border-color: transparent transparent transparent var(--color-text);
74
- opacity: 0.5;
75
- transition: transform 0.2s ease, opacity 0.15s ease;
76
- flex-shrink: 0;
77
- font-size: 0; /* Hide any text content */
78
- margin-right: 2px;
79
- }
80
-
81
- .process-group-header:hover .expand-icon {
82
- opacity: 0.8;
83
- }
84
-
85
- .process-group.expanded .process-group-header .expand-icon {
86
- transform: rotate(90deg);
87
- }
88
-
89
- /* Group icon removed - using status badges */
90
- .process-group-header .group-icon {
91
- display: none;
92
- }
93
-
94
- /* Group title - prominent display */
95
- .process-group-header .group-title {
96
- flex: 0 1 auto;
97
- font-size: var(--font-size-medium);
98
- font-weight: 500;
99
- color: var(--color-text-muted);
100
- opacity: 0.9;
101
- white-space: nowrap;
102
- overflow: hidden;
103
- text-overflow: ellipsis;
104
- max-width: 350px;
105
- }
106
-
107
- /* Step count badge */
108
- .process-group-header .step-count {
109
- font-size: 0.65rem;
110
- color: var(--color-text);
111
- opacity: 0.5;
112
- flex-shrink: 0;
113
- padding: 1px 6px;
114
- font-family: var(--font-family-code);
115
- }
116
-
117
- /* ===========================================
118
- 3-Letter Status Code Badges
119
- =========================================== */
120
-
121
- /* Base status badge */
122
- .step-badge {
123
- display: inline-flex;
124
- align-items: center;
125
- gap: 3px;
126
- padding: 2px 6px;
127
- border-radius: 3px;
128
- font-size: 0.65rem;
129
- font-weight: 600;
130
- font-family: var(--font-family-code);
131
- text-transform: uppercase;
132
- letter-spacing: 0.5px;
133
- flex-shrink: 0;
134
- line-height: 1;
135
- }
136
-
137
- /* Status badge with icon */
138
- .step-badge .material-symbols-outlined {
139
- font-size: 0.8rem;
140
- line-height: 1;
141
- }
142
-
143
- /* Badge icon styling */
144
- .step-badge .badge-icon {
145
- font-size: var(--font-size-xs);
146
- margin-right: 2px;
147
- opacity: 0.9;
148
- }
149
-
150
- /* Status colors */
151
- /* Each status defines --step-accent for cascading to internal icons */
152
-
153
- /* GEN - agent type (blue/cyan) */
154
- .process-group .GEN {
155
- --step-accent: #38bdf8;
156
- }
157
- .light-mode .process-group .GEN {
158
- --step-accent: #0284c7;
159
- }
160
-
161
- /* END - response/done type (green) */
162
- .process-group .END {
163
- --step-accent: #22c55e;
164
- }
165
- .light-mode .process-group .END {
166
- --step-accent: #15803d;
167
- }
168
-
169
- /* USE - tool usage (amber/yellow) */
170
- .process-group .USE {
171
- --step-accent: #fbbf24;
172
- }
173
- .light-mode .process-group .USE {
174
- --step-accent: #b45309;
175
- }
176
-
177
- /* MCP - mcp type (amber/yellow) */
178
- .process-group .MCP {
179
- --step-accent: #fbbf24;
180
- }
181
- .light-mode .process-group .MCP {
182
- --step-accent: #b45309;
183
- }
184
-
185
- /* SUB and RES - subagent type and response (teal) */
186
- .process-group .SUB {
187
- --step-accent: #14b8a6;
188
- }
189
- .light-mode .process-group .SUB {
190
- --step-accent: #0f766e;
191
- }
192
- .process-group .RES {
193
- --step-accent: #14b8a6;
194
- }
195
- .light-mode .process-group .RES {
196
- --step-accent: #0f766e;
197
- }
198
-
199
- /* EXE - code_exe type (magenta/purple) */
200
- .process-group .EXE {
201
- --step-accent: #ba68c8;
202
- }
203
- .light-mode .process-group .EXE {
204
- --step-accent: #7c3aed;
205
- }
206
-
207
- /* WWW - browser type (indigo) */
208
- .process-group .WWW {
209
- --step-accent: #818cf8;
210
- }
211
- .light-mode .process-group .WWW {
212
- --step-accent: #4f46e5;
213
- }
214
-
215
- /* WAIT - progress type (slate) */
216
- .process-group .HDL {
217
- --step-accent: #94a3b8;
218
- }
219
- .light-mode .process-group .HDL {
220
- --step-accent: #475569;
221
- }
222
-
223
- /* INF - info type (gray) */
224
- .process-group .INF {
225
- --step-accent: #94a3b8;
226
- }
227
- .light-mode .process-group .INF {
228
- --step-accent: #475569;
229
- }
230
-
231
- /* HNT - hint type (yellow-green) */
232
- .process-group .HNT {
233
- --step-accent: #a3e635;
234
- }
235
- .light-mode .process-group .HNT {
236
- --step-accent: #65a30d;
237
- }
238
-
239
- /* WRN - warning type (orange) */
240
- .process-group .WRN {
241
- --step-accent: #f97316;
242
- }
243
- .light-mode .process-group .WRN {
244
- --step-accent: #c2410c;
245
- }
246
-
247
-
248
- /* ERR - error type (red) */
249
- .process-group .ERR {
250
- --step-accent: var(--color-error-text);
251
- }
252
- .light-mode .process-group .ERR {
253
- --step-accent: var(--color-error-text);
254
- }
255
-
256
- /* UTL - util type (gray-blue) */
257
- .process-group .UTL {
258
- --step-accent: #64748b;
259
- }
260
- .light-mode .process-group .UTL {
261
- --step-accent: #334155;
262
- }
263
-
264
-
265
- .process-group .step-badge{
266
- color: var(--step-accent);
267
- }
268
-
269
- .process-group .kvps-key{
270
- color: var(--step-accent);
271
- }
272
-
273
-
274
- /* Completed process group styling */
275
- .process-group-completed {
276
- opacity: 0.95;
277
- }
278
-
279
- .process-group-completed .group-status {
280
- font-weight: 700;
281
- }
282
-
283
- /* Subtle completion indicator on the expand icon */
284
- .process-group-completed .expand-icon {
285
- color: #22c55e;
286
- }
287
-
288
- /* Group status badge in header */
289
- .process-group-header .group-status {
290
- margin-left: var(--spacing-xs);
291
- }
292
-
293
- /* Metrics row */
294
- .process-group-header .group-metrics {
295
- display: flex;
296
- align-items: center;
297
- gap: var(--spacing-sm);
298
- /* margin-left: auto; */
299
- margin-left: 2em;
300
- font-size: 0.65rem;
301
- font-family: var(--font-family-code);
302
- color: var(--color-text);
303
- opacity: 0.8;
304
- }
305
-
306
- .process-group-header .group-metrics .material-symbols-outlined {
307
- font-size: var(--font-size-xs);
308
- opacity: 0.7;
309
- }
310
-
311
- .process-group-header .group-metrics span[class^="metric-"] {
312
- display: inline-flex;
313
- align-items: center;
314
- gap: 2px;
315
- }
316
-
317
- .process-group-header .group-metrics .metric-notifications {
318
- font-weight: 600;
319
- }
320
-
321
- .process-group-header .group-metrics .metric-notifications[hidden] {
322
- display: none;
323
- }
324
-
325
- .process-group-header .group-metrics .metric-notifications .material-symbols-outlined {
326
- opacity: 0.85;
327
- }
328
-
329
- /* Legacy timestamp/duration (for backwards compatibility) */
330
- .process-group-header .group-timestamp {
331
- font-size: 0.65rem;
332
- color: rgba(255, 255, 255, 0.65);
333
- flex-shrink: 0;
334
- font-family: var(--font-family-code);
335
- display: none; /* Hidden, replaced by metrics */
336
- }
337
-
338
- .process-group-header .group-duration {
339
- font-size: var(--font-size-xs);
340
- color: #81c784;
341
- flex-shrink: 0;
342
- font-family: var(--font-family-code);
343
- min-width: 40px;
344
- text-align: right;
345
- display: none; /* Hidden, replaced by metrics */
346
- }
347
-
348
- /* Process Group Content - Animated expand/collapse */
349
- .process-group-content {
350
- display: grid;
351
- grid-template-rows: 0fr;
352
- opacity: 0;
353
- margin-top: 0;
354
- padding-top: 0;
355
- border-top: 1px solid transparent;
356
- transition: grid-template-rows 0.25s ease-out,
357
- opacity 0.2s ease-out,
358
- margin-top 0.25s ease-out,
359
- padding-top 0.25s ease-out,
360
- border-color 0.2s ease-out;
361
- overflow: hidden;
362
- color: var(--color-text-muted);
363
- max-width: 100%;
364
- }
365
-
366
- .process-group-content > .process-steps {
367
- min-height: 0;
368
- }
369
-
370
- .process-group.expanded .process-group-content {
371
- grid-template-rows: 1fr;
372
- opacity: 1;
373
- margin-top: var(--spacing-xs);
374
- padding-top: var(--spacing-xs);
375
- /* border-top-color: var(--color-border); */
376
- }
377
-
378
- /* Process Steps List */
379
- .process-steps {
380
- padding: 0;
381
- display: flex;
382
- flex-direction: column;
383
- gap: 2px;
384
- max-width: 100%;
385
- overflow: hidden;
386
- }
387
-
388
- .process-group-show-more {
389
- align-self: flex-start;
390
- appearance: none;
391
- display: inline-flex;
392
- align-items: center;
393
- border: 0;
394
- background: transparent;
395
- color: var(--color-text-muted);
396
- cursor: pointer;
397
- font-family: var(--font-family-main);
398
- font-size: var(--font-size-xs);
399
- padding: var(--spacing-xs) 0;
400
- opacity: 0.7;
401
- text-decoration: none;
402
- transition: opacity 0.15s ease;
403
- }
404
-
405
- .process-group-show-more:hover,
406
- .process-group-show-more:focus-visible {
407
- opacity: 1;
408
- }
409
-
410
- /* Individual Process Step */
411
- .process-step {
412
- display: flex;
413
- flex-direction: column;
414
- padding: 0.1em;
415
- transition: background-color 0.15s ease;
416
- max-width: 100%;
417
- overflow: hidden;
418
- }
419
-
420
- .process-step.message-util {
421
- display: none;
422
- }
423
-
424
- .show-utility-messages .process-step.message-util {
425
- display: flex;
426
- }
427
-
428
- /* Utility/Info/Hint steps have subtle background tint */
429
- .process-step[data-type="util"],
430
- .process-step[data-type="info"],
431
- .process-step[data-type="hint"] {
432
- background-color: rgba(120, 115, 100, 0);
433
- }
434
-
435
- .process-step[data-type="util"]:hover,
436
- .process-step[data-type="info"]:hover,
437
- .process-step[data-type="hint"]:hover {
438
- background-color: rgba(255, 255, 255, 0.03);
439
- }
440
-
441
-
442
- .light-mode .process-step[data-type="util"]:hover,
443
- .light-mode .process-step[data-type="info"]:hover,
444
- .light-mode .process-step[data-type="hint"]:hover {
445
- background-color: rgba(0, 0, 0, 0.04);
446
- }
447
-
448
- /* Step Header (clickable) */
449
- .process-step-header {
450
- display: flex;
451
- align-items: center;
452
- cursor: pointer;
453
- user-select: none;
454
- gap: 4px;
455
- min-height: 18px;
456
- max-width: 100%;
457
- overflow: hidden;
458
- }
459
-
460
- /* Step title */
461
- .process-step-header .step-title {
462
- flex: 1;
463
- font-size: 0.8rem;
464
- /* color: var(--color-text-muted); */
465
- white-space: nowrap;
466
- overflow: hidden;
467
- text-overflow: ellipsis;
468
- margin-left: var(--spacing-xs);
469
- min-width: 0;
470
- }
471
-
472
- /* Step expand icon - CSS triangle (no Material Icons) */
473
- .process-step-header .step-expand-icon {
474
- display: inline-block;
475
- width: 0;
476
- height: 0;
477
- border-style: solid;
478
- border-width: 3px 0 3px 5px;
479
- border-color: transparent transparent transparent var(--color-text);
480
- opacity: 0.4;
481
- transition: transform 0.2s ease, opacity 0.15s ease;
482
- flex-shrink: 0;
483
- font-size: 0; /* Hide any text content */
484
- margin-right: 2px;
485
- }
486
-
487
- .process-step-header:hover .step-expand-icon {
488
- opacity: 0.7;
489
- }
490
-
491
- /* Use direct child selector to prevent affecting nested steps */
492
- .process-step.expanded > .process-step-header > .step-expand-icon {
493
- transform: rotate(90deg);
494
- }
495
-
496
- /* Step Detail Content - Animated expand/collapse */
497
- .process-step-detail {
498
- /* display: grid; */
499
- grid-template-rows: 0fr;
500
- opacity: 0;
501
- margin-top: 0;
502
- transition: grid-template-rows 0.2s ease-out, opacity 0.15s ease-out, margin-top 0.2s ease-out;
503
- overflow: hidden;
504
- max-width: 100%;
505
- }
506
-
507
- .process-step-detail-content{
508
- white-space: pre-wrap;
509
- }
510
-
511
- .process-step-detail,
512
- .process-step-detail-scroll {
513
- scrollbar-width: thin; /* Firefox */
514
- -ms-overflow-style: thin; /* IE/Edge */
515
- }
516
-
517
- /* Hide scrollbar for Chrome/Safari/Webkit */
518
- .process-step-detail::-webkit-scrollbar,
519
- .process-step-detail-scroll::-webkit-scrollbar {
520
- width: thin;
521
- height: thin;
522
- }
523
-
524
- .process-step:not(.expanded) > .process-step-detail {
525
- margin-top: 0;
526
- }
527
-
528
- .process-step:not(.expanded) > .process-step-detail > .process-step-detail-scroll {
529
- padding: 0;
530
- margin-top: 0;
531
- max-height: 0;
532
- overflow: hidden;
533
- }
534
-
535
- .process-step-detail > .process-step-detail-scroll {
536
- min-height: 0;
537
- }
538
-
539
- /* Use direct child selector (>) to prevent cascading to nested steps */
540
- .process-step.expanded > .process-step-detail {
541
- /* grid-template-rows: 1fr; */
542
- opacity: 1;
543
- overflow: hidden;
544
- margin-top: var(--spacing-xs);
545
- max-width: 100%;
546
- padding-left: 17px;
547
- padding-right: 1em;
548
- }
549
-
550
- .process-step.expanded > .process-step-detail > .process-step-detail-scroll {
551
- max-height: 40em;
552
- overflow: auto;
553
- max-width: 100%;
554
- width: fit-content;
555
- box-sizing: content-box;
556
- border-radius: var(--border-radius-sm);
557
- -webkit-overflow-scrolling: touch;
558
- scrollbar-width: thin;
559
- -ms-overflow-style: thin;
560
- scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
561
- /* overscroll-behavior: contain; */
562
- }
563
-
564
- .process-step.expanded > .process-step-detail > .process-step-detail-scroll > .process-step-detail-scroll {
565
- scrollbar-width: thin;
566
- -ms-overflow-style: thin;
567
- scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
568
- }
569
-
570
- .process-step-detail-scroll {
571
- transition: max-height 0.2s ease-out, padding 0.2s ease-out, margin-top 0.2s ease-out;
572
- padding: var(--spacing-xs) 0;
573
- margin-top: var(--spacing-xxs);
574
- /* margin-left: 28px; */
575
- background-color: transparent;
576
- font-size: var(--font-size-xs);
577
- line-height: 1.5;
578
- -webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
579
- overscroll-behavior-x: contain;
580
- }
581
-
582
- /* .process-step-detail-scroll pre {
583
- margin: 0;
584
- white-space: pre-wrap;
585
- word-break: break-word;
586
- font-family: var(--font-family-code);
587
- font-size: var(--font-size-xs);
588
- color: var(--color-text);
589
- opacity: 0.8;
590
- } */
591
-
592
- /* KVPs in step detail - CSS Grid for aligned columns */
593
- .process-step-detail-scroll .step-kvps {
594
- display: grid;
595
- grid-template-columns: auto minmax(0, 1fr);
596
- gap: var(--spacing-xs) var(--spacing-sm);
597
- align-items: start;
598
- }
599
-
600
- .process-step-detail-scroll .step-kvp {
601
- display: contents; /* Children participate in parent grid */
602
- }
603
-
604
- .process-step-detail-scroll .step-kvp-key {
605
- color: var(--step-accent, var(--color-primary));
606
- font-weight: 500;
607
- opacity: 0.8;
608
- display: flex;
609
- align-items: center;
610
- justify-content: end;
611
- white-space: nowrap;
612
- }
613
-
614
- .process-step-detail-scroll .step-kvp-key .material-symbols-outlined {
615
- font-size: 0.9rem;
616
- color: var(--step-accent, var(--color-primary)) !important;
617
- }
618
-
619
- .process-step-detail-scroll .step-kvp-value {
620
- /* color: var(--color-text); */
621
- opacity: 0.85;
622
- word-break: break-word;
623
- overflow-wrap: anywhere;
624
- white-space: pre-wrap;
625
- font-size: 0.75rem;
626
- line-height: 1.5;
627
- min-width: 0;
628
- }
629
-
630
- .process-step-detail-scroll .step-kvp-value p {
631
- margin: 0;
632
- word-break: break-word;
633
- overflow-wrap: anywhere;
634
- }
635
-
636
-
637
- /* Tool arguments - CSS Grid for aligned columns */
638
- /* .process-step-detail-scroll .step-tool-args {
639
- display: grid;
640
- grid-template-columns: auto 1fr;
641
- gap: 4px 8px;
642
- margin: var(--spacing-xs) 0;
643
- align-items: baseline;
644
- } */
645
-
646
- /* .process-step-detail-scroll .tool-arg-row {
647
- display: contents;
648
- } */
649
-
650
- /* .process-step-detail-scroll .tool-arg-label {
651
- font-size: 0.68rem;
652
- font-weight: 600;
653
- color: var(--step-accent, var(--color-primary));
654
- display: flex;
655
- align-items: center;
656
- justify-content: end;
657
- } */
658
-
659
- /* .process-step-detail-scroll .tool-arg-label .material-symbols-outlined {
660
- font-size: 0.9rem;
661
- opacity: 0.85;
662
- color: var(--step-accent, var(--color-primary));
663
- } */
664
-
665
- /* .process-step-detail-scroll .tool-arg-value {
666
- font-size: var(--font-size-xs);
667
- color: var(--color-text);
668
- opacity: 0.85;
669
- word-break: break-word;
670
- font-family: var(--font-family-code);
671
- } */
672
-
673
- /* Light mode tool args - transparent like dark mode */
674
- /* .light-mode .process-step-detail-scroll .step-tool-args {
675
- background: transparent;
676
- border-left-color: transparent;
677
- } */
678
-
679
- /* .process-step-detail-scroll .step-tool-header .tool-icon {
680
- font-size: 0.85rem;
681
- color: var(--step-accent, var(--color-warning));
682
- opacity: 0.85;
683
- } */
684
-
685
- /* .process-step-detail-scroll .step-tool-header .tool-label {
686
- font-size: 0.68rem;
687
- font-weight: 600;
688
- color: var(--step-accent, var(--color-warning));
689
- opacity: 0.85;
690
- } */
691
-
692
- /* .process-step-detail-scroll .step-tool-header .tool-name {
693
- font-size: 0.72rem;
694
- color: var(--color-text);
695
- opacity: 0.85;
696
- } */
697
-
698
-
699
- /* Terminal output - self-contained scrollable block */
700
- .process-step-detail .terminal-output {
701
- margin: var(--spacing-xs) 0 0 0;
702
- padding: var(--spacing-xs);
703
- background: rgba(0, 0, 0, 0.8);
704
- min-height: 5em;
705
- min-width: 20em;
706
- width: max-content;
707
- border-radius: var(--border-radius-sm);
708
- color: #c9d1d9;
709
- white-space: pre;
710
- font-family: monospace;
711
- /* max-width: fit-content; */
712
- /* overflow: auto; */
713
- /* -webkit-overflow-scrolling: touch; */
714
- /* scrollbar-width: thin; */
715
- /* scrollbar-color: rgba(255,255,255,0.2) transparent; */
716
- /* overscroll-behavior: contain; */
717
- }
718
-
719
- .process-step-detail .terminal-output::-webkit-scrollbar {
720
- width: 4px;
721
- height: 4px;
722
- }
723
-
724
- .process-step-detail .terminal-output::-webkit-scrollbar-track {
725
- background: transparent;
726
- }
727
-
728
- .process-step-detail .terminal-output::-webkit-scrollbar-thumb {
729
- background: rgba(255, 255, 255, 0.2);
730
- border-radius: 2px;
731
- }
732
-
733
- .process-step-detail .terminal-output::-webkit-scrollbar-thumb:hover {
734
- background: rgba(255, 255, 255, 0.35);
735
- }
736
-
737
- /* Light mode terminal */
738
- .light-mode .process-step-detail .terminal-output {
739
- background: rgba(0, 0, 0, 0.05);
740
- color: black;
741
- scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
742
- }
743
-
744
- .light-mode .process-step-detail .terminal-output::-webkit-scrollbar-thumb {
745
- background: rgba(0, 0, 0, 0.15);
746
- }
747
-
748
- .light-mode .process-step-detail .terminal-output::-webkit-scrollbar-thumb:hover {
749
- background: rgba(0, 0, 0, 0.3);
750
- }
751
-
752
- /* Light mode adjustments */
753
- .light-mode .process-group.expanded .process-group-content {
754
- border-top-color: rgba(0, 0, 0, 0.06);
755
- }
756
-
757
- .light-mode .process-step-detail-scroll {
758
- background-color: transparent;
759
- }
760
-
761
- /* Light mode text colors for process group */
762
- .light-mode .process-group-header .group-title,
763
- .light-mode .process-step-header .step-title {
764
- color: var(--color-text-muted);
765
- }
766
-
767
-
768
-
769
- /* Animation for loading state */
770
- @keyframes pulse-step {
771
- 0%, 100% { opacity: 0.5; }
772
- 50% { opacity: 0.8; }
773
- }
774
-
775
- .step-title.shiny-text { color: transparent !important; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; }
776
- @keyframes shine { to { background-position: -100% center; } }
777
-
778
- /* Responsive adjustments */
779
- @media (max-width: 768px) {
780
- .process-group {
781
- padding: var(--spacing-xs) var(--spacing-sm);
782
- }
783
-
784
- .process-step-header .step-title {
785
- font-size: var(--font-size-xs);
786
- }
787
-
788
- .process-step-detail-scroll {
789
- margin-left: 0;
790
- }
791
- }
792
-
793
- /* ===========================================
794
- Preferences Visibility Controls
795
- These rules work with preferences-store toggles
796
- =========================================== */
797
-
798
- /* .process-step.message-util {
799
- display: none;
800
- }
801
-
802
- .step-kvp.msg-thoughts {
803
- display: none;
804
- }
805
-
806
- .process-step-detail-scroll pre.msg-json {
807
- display: none;
808
- } */
809
-
810
- /* Nested Process Steps (Subordinate Agents) */
811
-
812
- .process-nested-container {
813
- display: grid;
814
- grid-template-rows: 0fr;
815
- opacity: 0;
816
- overflow: hidden;
817
- transition: grid-template-rows 0.25s ease-out, opacity 0.2s ease-out;
818
- gap: 2px;
819
- max-width: 100%;
820
- }
821
-
822
- /* Only show nested container when parent step is expanded */
823
- .process-step.expanded > .process-nested-container {
824
- grid-template-rows: 1fr;
825
- opacity: 1;
826
- margin-top: 2px;
827
- /* Keep overflow hidden to allow nested steps to control their own expansion */
828
- }
829
-
830
- /* Inner wrapper to handle grid transition content */
831
- .process-nested-inner {
832
- min-height: 0;
833
- display: flex;
834
- flex-direction: column;
835
- gap: 2px;
836
- max-width: 100%;
837
- overflow: hidden;
838
- }
839
-
840
- /* All nested containers have same indentation (no cascading) */
841
- .nested-step .process-nested-container {
842
- margin-left: 0;
843
- }
844
-
845
- /* Response content in process steps */
846
- .process-step-detail-scroll .step-response-content {
847
- font-size: 0.75rem;
848
- line-height: 1.6;
849
- color: var(--color-text);
850
- opacity: 0.9;
851
- margin: var(--spacing-xs) 0;
852
- word-break: break-word;
853
- overflow-wrap: anywhere;
854
- }
855
-
856
- .process-step-detail-scroll .step-response-content p {
857
- margin: 0.5em 0;
858
- }
859
-
860
- .process-step-detail-scroll .step-response-content ul,
861
- .process-step-detail-scroll .step-response-content ol {
862
- margin: 0.5em 0;
863
- padding-left: 1.5em;
864
- }
865
-
866
- /* Warning/error content in process steps */
867
- .process-step-detail-scroll .step-warning-content {
868
- font-size: 0.72rem;
869
- line-height: 1.5;
870
- color: var(--color-warning);
871
- opacity: 0.9;
872
- margin: var(--spacing-xs) 0;
873
- padding: var(--spacing-xs);
874
- border-left: 2px solid var(--color-warning);
875
- border-radius: 3px;
876
- }
877
-
878
- /* Browser screenshot content in process steps */
879
- .process-step-detail-scroll .screenshot-img {
880
- max-width: 100%;
881
- max-height: 400px;
882
- border-radius: 4px;
883
- border: 1px solid rgba(255, 255, 255, 0.1);
884
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
885
- transition: box-shadow 0.2s ease, filter 0.2s ease;
886
- object-fit: contain;
887
- }
888
-
889
- .process-step-detail-scroll .screenshot-img:hover {
890
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
891
- filter: brightness(1.02);
892
- }
893
-
894
- /* Light mode screenshot border */
895
- .light-mode .process-step-detail-scroll .screenshot-img {
896
- border: 1px solid rgba(0, 0, 0, 0.15);
897
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
898
- }
899
-
900
- .light-mode .process-step-detail-scroll .screenshot-img:hover {
901
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
902
- }
903
-
904
- /* View Details button in step detail */
905
- .step-detail-actions {
906
- display: flex;
907
- padding-left: 18px;
908
- min-height: 0; /* Required for grid collapse animation */
909
- }
910
-
911
- /* Hide View Details button when step is collapsed */
912
- .process-step:not(.expanded) > .process-step-detail > .step-detail-actions {
913
- display: none;
914
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:41bbbb6cd4077b17682056b616b401c13223c16b38dd46fa0dd37cb729cb3011
3
+ size 21217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/messages/resize/message-resize-store.js CHANGED
@@ -1,137 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
- import { toggleCssProperty } from "/js/css.js";
3
-
4
- const model = {
5
- settings: {},
6
-
7
- async init() {
8
- this.settings =
9
- JSON.parse(localStorage.getItem("messageResizeSettings") || "null") ||
10
- this._getDefaultSettings();
11
- this._applyAllSettings();
12
- },
13
-
14
- _getDefaultSettings() {
15
- return {
16
- "message": { minimized: false, maximized: false },
17
- "message-agent": { minimized: true, maximized: false },
18
- "message-agent-response": { minimized: false, maximized: true },
19
- };
20
- },
21
-
22
- getSetting(className) {
23
- return this.settings[className] || { minimized: false, maximized: false };
24
- },
25
-
26
- _getDefaultSetting() {
27
- return { minimized: false, maximized: false };
28
- },
29
-
30
- _setSetting(className, setting) {
31
- this.settings[className] = setting;
32
- localStorage.setItem(
33
- "messageResizeSettings",
34
- JSON.stringify(this.settings)
35
- );
36
- },
37
-
38
- _applyAllSettings() {
39
- for (const [className, setting] of Object.entries(this.settings)) {
40
- this._applySetting(className, setting);
41
- }
42
- },
43
-
44
- async minimizeMessageClass(className, event) {
45
- const set = this.getSetting(className);
46
- set.minimized = !set.minimized;
47
- this._setSetting(className, set);
48
- this._applySetting(className, set);
49
- this._applyScroll(event);
50
- },
51
-
52
- async maximizeMessageClass(className, event) {
53
- const set = this.getSetting(className);
54
- if (set.minimized) return this.minimizeMessageClass(className, event); // if minimized, unminimize first
55
- set.maximized = !set.maximized;
56
- this._setSetting(className, set);
57
- this._applySetting(className, set);
58
- this._applyScroll(event);
59
- },
60
-
61
- _applyScroll(event) {
62
- if (!event || !event.target) {
63
- return;
64
- }
65
-
66
- // Store the element reference to avoid issues with event being modified
67
- const targetElement = event.target;
68
- const clickY = event.clientY;
69
-
70
- // Use requestAnimationFrame for smoother timing with browser rendering
71
- // requestAnimationFrame(() => {
72
- try {
73
- // Get fresh measurements after potential re-renders
74
- const rect = targetElement.getBoundingClientRect();
75
- const viewHeight = window.innerHeight || document.documentElement.clientHeight;
76
-
77
- // Get chat history element
78
- const chatHistory = document.getElementById('chat-history');
79
- if (!chatHistory) {
80
- return;
81
- }
82
-
83
- // Get chat history position
84
- const chatRect = chatHistory.getBoundingClientRect();
85
-
86
- // Calculate element's middle position relative to chat history
87
- const elementHeight = rect.height;
88
- const elementMiddle = rect.top + (elementHeight / 2);
89
- const relativeMiddle = elementMiddle - chatRect.top;
90
-
91
- // Calculate target scroll position
92
- let scrollTop;
93
-
94
- if (typeof clickY === 'number') {
95
- // Calculate based on click position
96
- const clickRelativeToChat = clickY - chatRect.top;
97
- // Add current scroll position and adjust to keep element middle at click position
98
- scrollTop = chatHistory.scrollTop + relativeMiddle - clickRelativeToChat;
99
- } else {
100
- // Position element middle at 50% from the top of chat history viewport (center)
101
- const targetPosition = chatHistory.clientHeight * 0.5;
102
- scrollTop = chatHistory.scrollTop + relativeMiddle - targetPosition;
103
- }
104
-
105
- // Apply scroll with instant behavior
106
- chatHistory.scrollTo({
107
- top: scrollTop,
108
- behavior: "auto"
109
- });
110
- } catch (e) {
111
- // Silent error handling
112
- }
113
- // });
114
- },
115
-
116
- _applySetting(className, setting) {
117
- toggleCssProperty(
118
- `.${className} .message-body`,
119
- "max-height",
120
- setting.maximized ? "unset" : "30em"
121
- );
122
- toggleCssProperty(
123
- `.${className} .message-body`,
124
- "overflow-y",
125
- setting.maximized ? "hidden" : "auto"
126
- );
127
- toggleCssProperty(
128
- `.${className} .message-body`,
129
- "display",
130
- setting.minimized ? "none" : "block"
131
- );
132
- },
133
- };
134
-
135
- const store = createStore("messageResize", model);
136
-
137
- export { store };
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3bc738ec8ebc2bc63e2dd68ede24dac25575f5f088a6e7e0a0f5bce139ac237d
3
+ size 4271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/modals/AGENTS.md CHANGED
@@ -1,34 +1,3 @@
1
- # Modal Components DOX
2
-
3
- ## Purpose
4
-
5
- - Own WebUI modal component content loaded through the shared modal stack.
6
-
7
- ## Ownership
8
-
9
- - Each direct child folder owns one modal workflow and its store.
10
- - Modal HTML files own body content, titles, scoped styles, and `data-modal-footer` content.
11
- - Modal store files own modal-local state and cleanup.
12
-
13
- ## Local Contracts
14
-
15
- - Use `openModal(path)` and `closeModal()` from `/js/modals.js`.
16
- - Keep footer content marked with `data-modal-footer` when using pinned modal actions.
17
- - Do not introduce a parallel overlay, backdrop, or teleport modal system.
18
-
19
- ## Work Guidance
20
-
21
- - Keep modal state cleanup explicit because stores may outlive DOM nodes.
22
- - Preserve stacked modal, Escape, click-outside, scroll, and footer behavior.
23
-
24
- ## Verification
25
-
26
- - Smoke-test open, close, Escape, click-outside, scrolling, stacked modals, and pinned footers after modal changes.
27
-
28
- ## Child DOX Index
29
-
30
- Direct child DOX files:
31
-
32
- | Child | Scope |
33
- | --- | --- |
34
- | [file-browser/AGENTS.md](file-browser/AGENTS.md) | File browser modal and right-canvas Files surface workflow. |
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7bf42ea967b7a8890f9db0e8cd5c4c144f231b7ab8c4b890c03b229fffb42b23
3
+ size 1086
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/components/modals/context/context-store.js CHANGED
@@ -1,132 +1,3 @@
1
- import { createStore } from "/js/AlpineStore.js";
2
-
3
- const model = {
4
- // State
5
- isLoading: false,
6
- contextData: null,
7
- tokenCount: 0,
8
- error: null,
9
- editor: null,
10
- closePromise: null,
11
-
12
- // Open Context Window modal
13
- async open() {
14
- if (this.isLoading) return; // Prevent double-open
15
-
16
- this.isLoading = true;
17
- this.error = null;
18
- this.contextData = null;
19
- this.tokenCount = 0;
20
-
21
- try {
22
- // Open modal FIRST (immediate UI feedback, but DON'T await)
23
- this.closePromise = window.openModal('modals/context/context.html');
24
-
25
- // Setup cleanup on modal close
26
- if (this.closePromise && typeof this.closePromise.then === 'function') {
27
- this.closePromise.then(() => {
28
- this.destroy();
29
- });
30
- }
31
-
32
- this.updateModalTitle(); // Set initial "loading" title
33
-
34
- // Fetch data from backend
35
- const contextId = window.getContext();
36
- const response = await window.sendJsonData('/ctx_window_get', {
37
- context: contextId,
38
- });
39
-
40
- // Update state with data
41
- this.contextData = response.content;
42
- this.tokenCount = response.tokens || 0;
43
- this.isLoading = false;
44
- this.updateModalTitle(); // Update with token count
45
-
46
- // Initialize ACE editor
47
- this.scheduleEditorInit();
48
-
49
- } catch (error) {
50
- console.error("Context fetch error:", error);
51
- this.error = error?.message || "Failed to load context window";
52
- this.isLoading = false;
53
- this.updateModalTitle(); // Show error in title
54
- }
55
- },
56
-
57
- scheduleEditorInit() {
58
- // Use double requestAnimationFrame to ensure DOM is ready
59
- window.requestAnimationFrame(() => {
60
- if (this.isLoading || this.error) return;
61
- window.requestAnimationFrame(() => this.initEditor());
62
- });
63
- },
64
-
65
- initEditor() {
66
- const container = document.getElementById("context-viewer-container");
67
- if (!container) {
68
- console.warn("Context container not found, deferring editor init");
69
- return;
70
- }
71
-
72
- // Destroy old instance if exists
73
- if (this.editor?.destroy) {
74
- this.editor.destroy();
75
- }
76
-
77
- // Check if ACE is available
78
- if (!window.ace?.edit) {
79
- console.error("ACE editor not available");
80
- this.error = "Editor library not loaded";
81
- return;
82
- }
83
-
84
- const editorInstance = window.ace.edit("context-viewer-container");
85
- if (!editorInstance) {
86
- console.error("Failed to create ACE editor instance");
87
- return;
88
- }
89
-
90
- this.editor = editorInstance;
91
-
92
- // Configure theme based on dark mode (legacy parity: != "false")
93
- const darkMode = window.localStorage?.getItem("darkMode");
94
- const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow";
95
-
96
- this.editor.setTheme(theme);
97
- this.editor.session.setMode("ace/mode/markdown");
98
- this.editor.setValue(this.contextData, -1); // -1 moves cursor to start
99
- this.editor.setReadOnly(true);
100
- this.editor.clearSelection();
101
- },
102
-
103
- updateModalTitle() {
104
- window.requestAnimationFrame(() => {
105
- const modalTitles = document.querySelectorAll(".modal.show .modal-title");
106
- if (!modalTitles.length) return;
107
-
108
- // Get the last (topmost) modal title
109
- const title = modalTitles[modalTitles.length - 1];
110
- if (!title) return;
111
-
112
- if (this.error) {
113
- title.textContent = "Context Window – Error";
114
- } else if (this.isLoading) {
115
- title.textContent = "Context Window (loading…)";
116
- } else {
117
- title.textContent = `Context Window ~${this.tokenCount} tokens`;
118
- }
119
- });
120
- },
121
-
122
- // Optional: cleanup method for lifecycle management
123
- destroy() {
124
- if (this.editor?.destroy) {
125
- this.editor.destroy();
126
- }
127
- this.editor = null;
128
- },
129
- };
130
-
131
- export const store = createStore("context", model);
132
-
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03662f99e944177b6d5227f6a7c2810bd8803aad119e8891690bfafe2ef1e495
3
+ size 3809