ui: welcome screen, queue input, interverntions
frdel committed
Jan 31, 2026 at 19:37 UTC
4aaf7af34a00d65db777bbb0a84ee7d19826c008
6 files changed
+173
-10
agent.py
+7
@@ -393,6 +393,7 @@ class Agent:
393
await self.call_extensions(
394
"message_loop_start", loop_data=self.loop_data
395
)
396
+ await self.handle_intervention()
397
398
try:
399
# prepare LLM chain (model, system, history)
@@ -402,6 +403,8 @@ class Agent:
403
await self.call_extensions(
404
"before_main_llm_call", loop_data=self.loop_data
405
)
406
+ await self.handle_intervention()
407
+
408
409
async def reasoning_callback(chunk: str, full: str):
410
await self.handle_intervention()
@@ -444,11 +447,14 @@ class Agent:
447
response_callback=stream_callback,
448
reasoning_callback=reasoning_callback,
449
)
450
+ await self.handle_intervention(agent_response)
451
452
# Notify extensions to finalize their stream filters
453
await self.call_extensions(
454
"reasoning_stream_end", loop_data=self.loop_data
455
)
456
+ await self.handle_intervention(agent_response)
457
+
458
await self.call_extensions(
459
"response_stream_end", loop_data=self.loop_data
460
)
@@ -581,6 +587,7 @@ class Agent:
587
"Critical error occurred, retrying..."
588
)
589
await asyncio.sleep(delay)
590
+ await self.handle_intervention()
591
agent_facing_error = self.read_prompt(
592
"fw.msg_critical_error.md", error_message=error_message
593
)
python/extensions/banners/_30_system_resources.py
new
+110
@@ -0,0 +1,110 @@
1
+from python.helpers.extension import Extension
2
+import os
3
+import psutil
4
+
5
+
6
+class SystemResourcesCheck(Extension):
7
+ async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
8
+ try:
9
+ cpu_percent = psutil.cpu_percent(interval=0.1)
10
+ except Exception:
11
+ cpu_percent = None
12
+
13
+ load_avg = self._get_load_average()
14
+
15
+ try:
16
+ vm = psutil.virtual_memory()
17
+ ram_percent = vm.percent
18
+ except Exception:
19
+ ram_percent = None
20
+
21
+ disk_percent, disk_path = self._get_disk_usage_percent()
22
+
23
+ try:
24
+ net = psutil.net_io_counters()
25
+ net_sent = self._format_bytes(net.bytes_sent)
26
+ net_recv = self._format_bytes(net.bytes_recv)
27
+ except Exception:
28
+ net_sent = "N/A"
29
+ net_recv = "N/A"
30
+
31
+ load_value = "N/A"
32
+ if load_avg:
33
+ la1, la5, la15 = load_avg
34
+ load_value = f"{la1:.2f} / {la5:.2f} / {la15:.2f}"
35
+
36
+ disk_value = "N/A"
37
+ if disk_percent is not None:
38
+ disk_value = f"{disk_percent:.0f}% ({disk_path})"
39
+
40
+ cpu_value = "N/A" if cpu_percent is None else f"{cpu_percent:.0f}%"
41
+ ram_value = "N/A" if ram_percent is None else f"{ram_percent:.0f}%"
42
+
43
+ cpu_bar = self._bar_html(cpu_percent)
44
+ ram_bar = self._bar_html(ram_percent)
45
+ disk_bar = self._bar_html(disk_percent)
46
+
47
+ banners.append({
48
+ "id": "system-resources",
49
+ "type": "info",
50
+ "priority": 10,
51
+ "title": "System Resources",
52
+ "html": (
53
+ "<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:12px 14px;\">"
54
+ "<div style=\"grid-column:1 / -1;display:grid;grid-template-columns:max-content 1fr;gap:6px 14px;align-items:center;\">"
55
+ f"<div style=\"font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.7\">CPU</div>"
56
+ f"<div style=\"display:flex;gap:10px;align-items:center;\"><div style=\"font-weight:700\">{cpu_value}</div>{cpu_bar}</div>"
57
+ f"<div style=\"font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.7\">RAM</div>"
58
+ f"<div style=\"display:flex;gap:10px;align-items:center;\"><div style=\"font-weight:700\">{ram_value}</div>{ram_bar}</div>"
59
+ f"<div style=\"font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.7\">Disk</div>"
60
+ f"<div style=\"display:flex;gap:10px;align-items:center;\"><div style=\"font-weight:700\">{disk_value}</div>{disk_bar}</div>"
61
+ "</div>"
62
+ f"<div style=\"padding-top:2px;\"><div style=\"font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.7;margin-bottom:4px;\">Load (1/5/15)</div><div style=\"font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;opacity:.85\">{load_value}</div></div>"
63
+ f"<div style=\"padding-top:2px;\"><div style=\"font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.7;margin-bottom:4px;\">Net (since boot)</div><div style=\"font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;opacity:.85\">{net_sent} sent / {net_recv} recv</div></div>"
64
+ "</div>"
65
+ ),
66
+ "dismissible": False,
67
+ "source": "backend",
68
+ })
69
+
70
+ def _bar_html(self, percent: float | None) -> str:
71
+ if percent is None:
72
+ return ""
73
+
74
+ p = max(0.0, min(100.0, float(percent)))
75
+ if p >= 85:
76
+ color = "#ef4444"
77
+ elif p >= 70:
78
+ color = "#f59e0b"
79
+ else:
80
+ color = "#22c55e"
81
+
82
+ return (
83
+ "<div style=\"flex:1;min-width:120px;max-width:220px;height:8px;border-radius:999px;"
84
+ "background:rgba(255,255,255,.10);overflow:hidden;\">"
85
+ f"<div style=\"height:100%;width:{p:.0f}%;background:{color};border-radius:999px;\"></div>"
86
+ "</div>"
87
+ )
88
+
89
+ def _get_load_average(self) -> tuple[float, float, float] | None:
90
+ try:
91
+ return os.getloadavg()
92
+ except Exception:
93
+ return None
94
+
95
+ def _get_disk_usage_percent(self) -> tuple[float | None, str]:
96
+ for path in ["/", os.path.expanduser("~")]:
97
+ try:
98
+ usage = psutil.disk_usage(path)
99
+ return usage.percent, path
100
+ except Exception:
101
+ continue
102
+ return None, "/"
103
+
104
+ def _format_bytes(self, value: int) -> str:
105
+ size = float(value)
106
+ for unit in ["B", "KB", "MB", "GB", "TB", "PB"]:
107
+ if size < 1024:
108
+ return f"{size:.1f} {unit}"
109
+ size /= 1024
110
+ return f"{size:.1f} EB"
webui/components/chat/input/chat-bar-input.html
+4
-2
@@ -5,6 +5,7 @@
5
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6
import { store as fullScreenStore } from "/components/modals/full-screen-input/full-screen-store.js";
7
import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
8
+ import { store as chatInputStore } from "/components/chat/input/input-store.js";
9
</script>
10
</head>
11
<body>
@@ -21,8 +22,9 @@
22
23
<!-- Container for textarea and expand button -->
24
<div id="chat-input-container" style="position: relative;">
24
- <textarea id="chat-input" placeholder="Type your message here..." rows="1"
25
-+ @keydown.enter="if (!$event.shiftKey && !$event.isComposing && $event.keyCode !== 229) { $event.preventDefault(); $store.chatInput.sendMessage(); }"
25
+ <textarea id="chat-input" :placeholder="$store.chatInput.inputPlaceholder" rows="1"
26
+ x-effect="$el.placeholder = $store.chatInput.inputPlaceholder"
27
+ @keydown.enter="if (!$event.shiftKey && !$event.isComposing && $event.keyCode !== 229) { $event.preventDefault(); $store.chatInput.sendMessage(); }"
28
@input="$store.chatInput.adjustTextareaHeight()"></textarea>
29
<button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
30
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
webui/components/chat/input/input-store.js
+11
@@ -8,6 +8,17 @@ import { store as attachmentsStore } from "/components/chat/attachments/attachme
8
const model = {
9
paused: false,
10
11
+ get inputPlaceholder() {
12
+ const input = document.getElementById("chat-input");
13
+ const hasTypedText = !!input?.value?.trim();
14
+ const hasAttachments = (attachmentsStore?.attachments?.length || 0) > 0;
15
+ const hasQueue = !!messageQueueStore?.hasQueue;
16
+
17
+ if (hasQueue && !hasTypedText && !hasAttachments)
18
+ return "Press Enter to send queued messages";
19
+ return "Type your message here...";
20
+ },
21
+
22
// Computed: send button icon type
23
get sendButtonIcon() {
24
const input = document.getElementById("chat-input");
webui/components/welcome/welcome-screen.html
+39
-6
@@ -397,22 +397,22 @@
397
}
398
399
.welcome-actions {
400
- grid-template-columns: repeat(2, 1fr);
401
- gap: 1rem;
402
- margin-bottom: 2rem;
400
+ grid-template-columns: repeat(4, 1fr);
401
+ gap: 0.75rem;
402
+ margin-bottom: 1.5rem;
403
}
404
405
.welcome-action-card {
406
- padding: 1rem;
406
aspect-ratio: 1 / 1;
407
+ padding: 0.5rem;
408
}
409
410
.welcome-action-icon {
411
- font-size: 2rem;
411
+ font-size: 1.6rem;
412
}
413
414
.welcome-action-title {
415
- font-size: 1.1rem;
415
+ font-size: 0.85rem;
416
}
417
418
.welcome-banners {
@@ -431,6 +431,39 @@
431
font-size: 0.8rem;
432
}
433
}
434
+
435
+ @media (max-width: 520px) {
436
+ .welcome-actions {
437
+ grid-template-columns: 1fr;
438
+ gap: 0.5rem;
439
+ }
440
+
441
+ .welcome-action-card {
442
+ aspect-ratio: auto;
443
+ padding: 0.5rem 0.75rem;
444
+ flex-direction: row;
445
+ justify-content: flex-start;
446
+ align-items: center;
447
+ text-align: left;
448
+ border-radius: 10px;
449
+ min-height: 44px;
450
+ }
451
+
452
+ .welcome-action-icon {
453
+ margin-bottom: 0;
454
+ margin-right: 0.5rem;
455
+ font-size: 1.35rem;
456
+ line-height: 1;
457
+ display: flex;
458
+ align-items: center;
459
+ }
460
+
461
+ .welcome-action-title {
462
+ font-size: 0.95rem;
463
+ margin: 0;
464
+ line-height: 1.2;
465
+ }
466
+ }
467
</style>
468
</body>
469
webui/components/welcome/welcome-store.js
+2
-2
@@ -93,12 +93,12 @@ const model = {
93
const bannerMap = new Map();
94
95
for (const banner of frontendBanners) {
96
- if (banner.id && !dismissed.has(banner.id)) {
96
+ if (banner.id && (banner.dismissible === false || !dismissed.has(banner.id))) {
97
bannerMap.set(banner.id, banner);
98
}
99
}
100
for (const banner of backendBanners) {
101
- if (banner.id && !dismissed.has(banner.id)) {
101
+ if (banner.id && (banner.dismissible === false || !dismissed.has(banner.id))) {
102
bannerMap.set(banner.id, banner);
103
}
104
}