rename chat_compaction plugin to match convention

Nicolas Leão committed Mar 26, 2026 at 11:01 UTC 9fc2d9570f995e156571b8f2414368926052951c
21 files changed +14 -559
.vscode/launch.json
+1 -1
@@ -9,7 +9,7 @@
9 "program": "./run_ui.py",
10 "console": "integratedTerminal",
11 "justMyCode": false,
12 - "args": ["--development=true", "-Xfrozen_modules=off"]
12 + "args": ["--development=true", "--port=5555", "-Xfrozen_modules=off"]
13 },
14 {
15 "name": "Debug current file",
plugins/_chat_compaction/.toggle-1 renamed
plugins/_chat_compaction/api/compact_chat.py renamed
+8 -19
@@ -1,7 +1,10 @@
1 """API handler for chat compaction."""
2 -import importlib
2 from helpers.api import ApiHandler, Input, Output, Request, Response
3 from agent import AgentContext
4 +from plugins._chat_compaction.helpers.compactor import (
5 + get_compaction_stats,
6 + run_compaction,
7 +)
8
9
10 class CompactChat(ApiHandler):
@@ -18,33 +21,23 @@ class CompactChat(ApiHandler):
21 if not context:
22 return Response("Context not found", 404)
23
21 - # Validate context is not running
24 if context.is_running():
25 return Response("Cannot compact while agent is running", 409)
26
25 - # Check if there's enough content to compact
26 - # Count user-visible log items (what the user sees in the UI)
27 visible_count = len(context.log.logs)
28 if visible_count <= 1:
29 return Response("Not enough messages to compact", 400)
30
31 - # Force reload compactor module to pick up latest changes
32 - import usr.plugins.compaction.helpers.compactor as _compactor_mod
33 - importlib.reload(_compactor_mod)
34 -
31 if action == "stats":
36 - # Return statistics for confirmation modal
37 - stats = await _compactor_mod.get_compaction_stats(context)
32 + stats = await get_compaction_stats(context)
33 return {"ok": True, "stats": stats}
34
35 elif action == "compact":
41 - # Get plugin config for model choice
36 from helpers.plugins import get_plugin_config
37 agent = context.agent0
44 - plugin_config = get_plugin_config("compaction", agent=agent) or {}
38 + plugin_config = get_plugin_config("_chat_compaction", agent=agent) or {}
39 use_chat_model = plugin_config.get("use_chat_model", True)
40
47 - # Start compaction as a deferred task
41 context.run_task(_run_compaction_task, context, use_chat_model)
42
43 return {"ok": True, "message": "Compaction started"}
@@ -56,16 +49,12 @@ class CompactChat(ApiHandler):
49 async def _run_compaction_task(context, use_chat_model: bool):
50 """Wrapper to run compaction and handle errors."""
51 try:
59 - import importlib
60 - import usr.plugins.compaction.helpers.compactor as _compactor_mod
61 - importlib.reload(_compactor_mod)
62 - await _compactor_mod.run_compaction(context, use_chat_model)
52 + await run_compaction(context, use_chat_model)
53 except Exception as e:
64 - # Log error but don't crash the task
54 context.log.log(
55 type="error",
56 heading="Compaction Failed",
57 content=str(e),
58 )
59 from helpers.state_monitor_integration import mark_dirty_all
71 - mark_dirty_all(reason="plugins.compaction.compact_chat_error")
60 + mark_dirty_all(reason="plugins._chat_compaction.compact_chat_error")
plugins/_chat_compaction/default_config.yaml renamed
plugins/_chat_compaction/extensions/webui/chat-input-bottom-actions-start/compact-button.html renamed
+1 -1
@@ -1,7 +1,7 @@
1 <html>
2 <head>
3 <title>Compact Button Extension</title>
4 - <script type="module" src="/plugins/compaction/webui/compact-store.js"></script>
4 + <script type="module" src="/plugins/_chat_compaction/webui/compact-store.js"></script>
5 </head>
6 <body>
7 <div x-data="{
plugins/_chat_compaction/helpers/compactor.py renamed
plugins/_chat_compaction/plugin.yaml renamed
+1 -1
@@ -1,4 +1,4 @@
1 -name: compaction
1 +name: _chat_compaction
2 title: Chat Compaction
3 description: Compact entire chat history into a single optimized summary message.
4 version: 1.0.0
plugins/_chat_compaction/prompts/compact.msg.md renamed
plugins/_chat_compaction/prompts/compact.sys.md renamed
plugins/_chat_compaction/webui/compact-modal.html renamed
+1 -1
@@ -255,7 +255,7 @@
255 </style>
256
257 <script type="module">
258 - import { store } from "/plugins/compaction/webui/compact-store.js";
258 + import { store } from "/plugins/_chat_compaction/webui/compact-store.js";
259 </script>
260 </body>
261 </html>
plugins/_chat_compaction/webui/compact-store.js renamed
+2 -2
@@ -18,7 +18,7 @@ export const store = createStore("compactStore", {
18 return;
19 }
20
21 - const res = await callJsonApi("/plugins/compaction/compact_chat", {
21 + const res = await callJsonApi("/plugins/_chat_compaction/compact_chat", {
22 context: ctxid,
23 action: "stats",
24 });
@@ -42,7 +42,7 @@ export const store = createStore("compactStore", {
42 throw new Error("No active chat");
43 }
44
45 - const res = await callJsonApi("/plugins/compaction/compact_chat", {
45 + const res = await callJsonApi("/plugins/_chat_compaction/compact_chat", {
46 context: ctxid,
47 action: "compact",
48 });
plugins/_chat_compaction/webui/config.html renamed
plugins/chat_compaction/compaction/.toggle-1
plugins/chat_compaction/compaction/api/compact_chat.py deleted
-71
@@ -1,71 +0,0 @@
1 -"""API handler for chat compaction."""
2 -import importlib
3 -from helpers.api import ApiHandler, Input, Output, Request, Response
4 -from agent import AgentContext
5 -
6 -
7 -class CompactChat(ApiHandler):
8 - """Compact the current chat history into a summarized message."""
9 -
10 - async def process(self, input: Input, request: Request) -> Output:
11 - ctxid = input.get("context", "")
12 - action = input.get("action", "compact")
13 -
14 - if not ctxid:
15 - return Response("Missing context id", 400)
16 -
17 - context = AgentContext.get(ctxid)
18 - if not context:
19 - return Response("Context not found", 404)
20 -
21 - # Validate context is not running
22 - if context.is_running():
23 - return Response("Cannot compact while agent is running", 409)
24 -
25 - # Check if there's enough content to compact
26 - # Count user-visible log items (what the user sees in the UI)
27 - visible_count = len(context.log.logs)
28 - if visible_count <= 1:
29 - return Response("Not enough messages to compact", 400)
30 -
31 - # Force reload compactor module to pick up latest changes
32 - import usr.plugins.compaction.helpers.compactor as _compactor_mod
33 - importlib.reload(_compactor_mod)
34 -
35 - if action == "stats":
36 - # Return statistics for confirmation modal
37 - stats = await _compactor_mod.get_compaction_stats(context)
38 - return {"ok": True, "stats": stats}
39 -
40 - elif action == "compact":
41 - # Get plugin config for model choice
42 - from helpers.plugins import get_plugin_config
43 - agent = context.agent0
44 - plugin_config = get_plugin_config("compaction", agent=agent) or {}
45 - use_chat_model = plugin_config.get("use_chat_model", True)
46 -
47 - # Start compaction as a deferred task
48 - context.run_task(_run_compaction_task, context, use_chat_model)
49 -
50 - return {"ok": True, "message": "Compaction started"}
51 -
52 - else:
53 - return Response(f"Unknown action: {action}", 400)
54 -
55 -
56 -async def _run_compaction_task(context, use_chat_model: bool):
57 - """Wrapper to run compaction and handle errors."""
58 - try:
59 - import importlib
60 - import usr.plugins.compaction.helpers.compactor as _compactor_mod
61 - importlib.reload(_compactor_mod)
62 - await _compactor_mod.run_compaction(context, use_chat_model)
63 - except Exception as e:
64 - # Log error but don't crash the task
65 - context.log.log(
66 - type="error",
67 - heading="Compaction Failed",
68 - content=str(e),
69 - )
70 - from helpers.state_monitor_integration import mark_dirty_all
71 - mark_dirty_all(reason="plugins.compaction.compact_chat_error")
plugins/chat_compaction/compaction/plugin.yaml deleted
-8
@@ -1,8 +0,0 @@
1 -name: compaction
2 -title: Chat Compaction
3 -description: Compact entire chat history into a single optimized summary message.
4 -version: 1.0.0
5 -settings_sections:
6 - - agent
7 -per_project_config: false
8 -per_agent_config: false
plugins/chat_compaction/default_config.yaml deleted
-1
@@ -1 +0,0 @@
1 -use_chat_model: true
plugins/chat_compaction/prompts/compact.msg.md deleted
-5
@@ -1,5 +0,0 @@
1 -Compact this conversation to its essential facts. Be maximally concise.
2 -
3 ----
4 -{{conversation}}
5 ----
plugins/chat_compaction/prompts/compact.sys.md deleted
-17
@@ -1,17 +0,0 @@
1 -You are a conversation compactor. Produce the most concise summary possible while preserving critical information.
2 -
3 -Rules:
4 -- Extract only: key decisions, final outcomes, actionable facts, unresolved items
5 -- Discard: intermediate reasoning, failed attempts, redundant exchanges, pleasantries
6 -- Use terse bullet points, not prose
7 -- Collapse related items into single lines
8 -- Keep exact values: file paths, config values, code identifiers, credentials, URLs
9 -- Omit anything that can be re-derived from context
10 -- Group by topic, not chronology
11 -- No meta-commentary about the summarization
12 -- Target: 10-20% of original length
13 -
14 -Output format:
15 -- Markdown with short section headers
16 -- Bullet lists, no paragraphs
17 -- Code/paths in backticks inline, not fenced blocks unless multi-line
plugins/chat_compaction/webui/compact-modal.html deleted
-261
@@ -1,261 +0,0 @@
1 -<html>
2 -<head>
3 - <title>Compact Chat</title>
4 -</head>
5 -<body>
6 - <div x-data x-show="$store.compactStore?.showModal" style="display: none;">
7 - <div class="modal-overlay" @click="$store.compactStore?.closeModal()">
8 - <div class="modal-container" @click.stop>
9 - <div class="modal-header">
10 - <h3>Compact Chat History</h3>
11 - <button class="modal-close" @click="$store.compactStore?.closeModal()">
12 - <span class="material-symbols-outlined">close</span>
13 - </button>
14 - </div>
15 -
16 - <div class="modal-content">
17 - <template x-if="$store.compactStore?.stats">
18 - <div class="stats-container">
19 - <p class="stats-description">
20 - This will summarize your entire conversation into a single optimized message.
21 - </p>
22 -
23 - <div class="stats-grid">
24 - <div class="stat-item">
25 - <span class="stat-label">Messages</span>
26 - <span class="stat-value" x-text="$store.compactStore?.stats?.message_count"></span>
27 - </div>
28 - <div class="stat-item">
29 - <span class="stat-label">Tokens</span>
30 - <span class="stat-value" x-text="$store.compactStore?.stats?.token_count?.toLocaleString()"></span>
31 - </div>
32 - <div class="stat-item">
33 - <span class="stat-label">Model</span>
34 - <span class="stat-value" x-text="$store.compactStore?.stats?.model_name"></span>
35 - </div>
36 - </div>
37 -
38 - <div class="stats-warning">
39 - <span class="material-symbols-outlined">warning</span>
40 - <span>This action cannot be undone. The original conversation will be replaced with a summary.</span>
41 - </div>
42 - </div>
43 - </template>
44 -
45 - <template x-if="!$store.compactStore?.stats">
46 - <div class="stats-loading">
47 - <span class="loading-spinner"></span>
48 - <span>Loading statistics...</span>
49 - </div>
50 - </template>
51 - </div>
52 -
53 - <div class="modal-footer">
54 - <button class="button secondary" @click="$store.compactStore?.closeModal()">
55 - Cancel
56 - </button>
57 - <button
58 - class="button primary danger"
59 - @click="$store.compactStore?.compact()"
60 - :disabled="$store.compactStore?.compacting || !$store.compactStore?.stats"
61 - >
62 - <template x-if="$store.compactStore?.compacting">
63 - <span class="loading-spinner"></span>
64 - </template>
65 - <span>Compact</span>
66 - </button>
67 - </div>
68 - </div>
69 - </div>
70 - </div>
71 -
72 - <style>
73 - .modal-overlay {
74 - position: fixed;
75 - top: 0;
76 - left: 0;
77 - right: 0;
78 - bottom: 0;
79 - background: rgba(0, 0, 0, 0.5);
80 - display: flex;
81 - align-items: center;
82 - justify-content: center;
83 - z-index: 1000;
84 - }
85 -
86 - .modal-container {
87 - background: var(--color-background-elevated);
88 - border-radius: var(--border-radius-md);
89 - box-shadow: var(--shadow-lg);
90 - width: 90%;
91 - max-width: 500px;
92 - max-height: 90vh;
93 - overflow-y: auto;
94 - }
95 -
96 - .modal-header {
97 - display: flex;
98 - align-items: center;
99 - justify-content: space-between;
100 - padding: var(--spacing-md) var(--spacing-lg);
101 - border-bottom: 1px solid var(--color-border);
102 - }
103 -
104 - .modal-header h3 {
105 - margin: 0;
106 - font-size: 1.1rem;
107 - font-weight: 600;
108 - }
109 -
110 - .modal-close {
111 - background: transparent;
112 - border: none;
113 - cursor: pointer;
114 - padding: var(--spacing-xs);
115 - border-radius: var(--border-radius-sm);
116 - color: var(--color-text-secondary);
117 - transition: color 0.2s;
118 - }
119 -
120 - .modal-close:hover {
121 - color: var(--color-text);
122 - }
123 -
124 - .modal-content {
125 - padding: var(--spacing-lg);
126 - }
127 -
128 - .stats-description {
129 - margin: 0 0 var(--spacing-md) 0;
130 - color: var(--color-text-secondary);
131 - font-size: 0.9rem;
132 - }
133 -
134 - .stats-grid {
135 - display: grid;
136 - grid-template-columns: repeat(3, 1fr);
137 - gap: var(--spacing-md);
138 - margin-bottom: var(--spacing-lg);
139 - }
140 -
141 - .stat-item {
142 - display: flex;
143 - flex-direction: column;
144 - align-items: center;
145 - padding: var(--spacing-md);
146 - background: var(--color-background-muted);
147 - border-radius: var(--border-radius-md);
148 - }
149 -
150 - .stat-label {
151 - font-size: 0.75rem;
152 - color: var(--color-text-secondary);
153 - text-transform: uppercase;
154 - letter-spacing: 0.05em;
155 - margin-bottom: var(--spacing-xs);
156 - }
157 -
158 - .stat-value {
159 - font-size: 1.25rem;
160 - font-weight: 600;
161 - color: var(--color-text);
162 - }
163 -
164 - .stats-warning {
165 - display: flex;
166 - align-items: flex-start;
167 - gap: var(--spacing-sm);
168 - padding: var(--spacing-md);
169 - background: var(--color-warning-bg, rgba(245, 158, 11, 0.1));
170 - border: 1px solid var(--color-warning-border, rgba(245, 158, 11, 0.3));
171 - border-radius: var(--border-radius-md);
172 - color: var(--color-warning-text, #92400e);
173 - font-size: 0.85rem;
174 - }
175 -
176 - .stats-warning .material-symbols-outlined {
177 - font-size: 1.2rem;
178 - flex-shrink: 0;
179 - }
180 -
181 - .stats-loading {
182 - display: flex;
183 - align-items: center;
184 - justify-content: center;
185 - gap: var(--spacing-sm);
186 - padding: var(--spacing-xl);
187 - color: var(--color-text-secondary);
188 - }
189 -
190 - .modal-footer {
191 - display: flex;
192 - justify-content: flex-end;
193 - gap: var(--spacing-sm);
194 - padding: var(--spacing-md) var(--spacing-lg);
195 - border-top: 1px solid var(--color-border);
196 - }
197 -
198 - .button {
199 - padding: var(--spacing-sm) var(--spacing-md);
200 - border-radius: var(--border-radius-md);
201 - font-size: 0.9rem;
202 - font-weight: 500;
203 - cursor: pointer;
204 - transition: all 0.2s;
205 - display: flex;
206 - align-items: center;
207 - gap: var(--spacing-xs);
208 - }
209 -
210 - .button:disabled {
211 - opacity: 0.6;
212 - cursor: not-allowed;
213 - }
214 -
215 - .button.secondary {
216 - background: var(--color-background-muted);
217 - border: 1px solid var(--color-border);
218 - color: var(--color-text);
219 - }
220 -
221 - .button.secondary:hover:not(:disabled) {
222 - background: var(--color-background-hover);
223 - }
224 -
225 - .button.primary {
226 - background: var(--color-primary);
227 - border: none;
228 - color: white;
229 - }
230 -
231 - .button.primary:hover:not(:disabled) {
232 - background: var(--color-primary-hover);
233 - }
234 -
235 - .button.danger {
236 - background: var(--color-danger, #dc2626);
237 - }
238 -
239 - .button.danger:hover:not(:disabled) {
240 - background: var(--color-danger-hover, #b91c1c);
241 - }
242 -
243 - .loading-spinner {
244 - width: 16px;
245 - height: 16px;
246 - border: 2px solid currentColor;
247 - border-top-color: transparent;
248 - border-radius: 50%;
249 - animation: spin 0.8s linear infinite;
250 - }
251 -
252 - @keyframes spin {
253 - to { transform: rotate(360deg); }
254 - }
255 - </style>
256 -
257 - <script type="module">
258 - import { store } from "/plugins/compaction/webui/compact-store.js";
259 - </script>
260 -</body>
261 -</html>
plugins/chat_compaction/webui/compact-store.js deleted
-67
@@ -1,67 +0,0 @@
1 -import { createStore } from "/js/AlpineStore.js";
2 -import { callJsonApi } from "/js/api.js";
3 -import {
4 - toastFrontendSuccess,
5 - toastFrontendError,
6 -} from "/components/notifications/notification-store.js";
7 -
8 -export const store = createStore("compactStore", {
9 - compacting: false,
10 - stats: null,
11 - showModal: false,
12 -
13 - async fetchStats() {
14 - try {
15 - const ctxid = globalThis.getContext?.();
16 - if (!ctxid) {
17 - toastFrontendError("No active chat", "Compaction");
18 - return;
19 - }
20 -
21 - const res = await callJsonApi("/plugins/compaction/compact_chat", {
22 - context: ctxid,
23 - action: "stats",
24 - });
25 -
26 - if (!res?.ok) {
27 - throw new Error(res?.message || "Failed to fetch stats");
28 - }
29 -
30 - this.stats = res.stats;
31 - this.showModal = true;
32 - } catch (e) {
33 - toastFrontendError(e.message, "Compaction");
34 - }
35 - },
36 -
37 - async compact() {
38 - this.compacting = true;
39 - try {
40 - const ctxid = globalThis.getContext?.();
41 - if (!ctxid) {
42 - throw new Error("No active chat");
43 - }
44 -
45 - const res = await callJsonApi("/plugins/compaction/compact_chat", {
46 - context: ctxid,
47 - action: "compact",
48 - });
49 -
50 - if (!res?.ok) {
51 - throw new Error(res?.message || "Compaction failed");
52 - }
53 -
54 - toastFrontendSuccess("Compaction started", "Compaction");
55 - this.showModal = false;
56 - } catch (e) {
57 - toastFrontendError(e.message, "Compaction");
58 - } finally {
59 - this.compacting = false;
60 - }
61 - },
62 -
63 - closeModal() {
64 - this.showModal = false;
65 - this.stats = null;
66 - },
67 -});
plugins/chat_compaction/webui/config.html deleted
-104
@@ -1,104 +0,0 @@
1 -<html>
2 -<head>
3 - <title>Compaction Plugin Settings</title>
4 -</head>
5 -<body>
6 - <div x-data>
7 - <template x-if="config">
8 - <div>
9 - <div class="section-title">Compaction Configuration</div>
10 - <div class="section-description">
11 - Configure how the chat compaction feature works.
12 - </div>
13 -
14 - <div class="field">
15 - <div class="field-label">
16 - <div class="field-title">Use chat model</div>
17 - <div class="field-description">
18 - When enabled, uses the currently selected chat model for compaction.
19 - When disabled, uses the utility model (usually faster and cheaper).
20 - </div>
21 - </div>
22 - <div class="field-control">
23 - <label class="toggle">
24 - <input type="checkbox" x-model="config.use_chat_model" />
25 - <span class="toggler"></span>
26 - </label>
27 - </div>
28 - </div>
29 - </div>
30 - </template>
31 - </div>
32 -
33 - <style>
34 - .section-title {
35 - font-size: 1rem;
36 - font-weight: 600;
37 - margin-bottom: 8px;
38 - color: var(--color-text, #e5e5e5);
39 - }
40 - .section-description {
41 - font-size: 0.85rem;
42 - color: var(--color-text-secondary, #999);
43 - margin-bottom: 20px;
44 - line-height: 1.5;
45 - }
46 - .field {
47 - margin-bottom: 20px;
48 - }
49 - .field-label {
50 - margin-bottom: 8px;
51 - }
52 - .field-title {
53 - font-weight: 500;
54 - color: var(--color-text, #e5e5e5);
55 - margin-bottom: 4px;
56 - }
57 - .field-description {
58 - font-size: 0.8rem;
59 - color: var(--color-text-secondary, #999);
60 - line-height: 1.4;
61 - }
62 - .field-control {
63 - margin-top: 8px;
64 - }
65 - .toggle {
66 - display: inline-flex;
67 - align-items: center;
68 - cursor: pointer;
69 - }
70 - .toggle input {
71 - display: none;
72 - }
73 - .toggler {
74 - width: 44px;
75 - height: 24px;
76 - background: var(--color-border, #444);
77 - border-radius: 12px;
78 - position: relative;
79 - transition: background 0.2s;
80 - }
81 - .toggler::after {
82 - content: '';
83 - position: absolute;
84 - width: 20px;
85 - height: 20px;
86 - background: white;
87 - border-radius: 50%;
88 - top: 2px;
89 - left: 2px;
90 - transition: transform 0.2s;
91 - }
92 - .toggle input:checked + .toggler {
93 - background: var(--color-primary, #3b82f6);
94 - }
95 - .toggle input:checked + .toggler::after {
96 - transform: translateX(20px);
97 - }
98 - </style>
99 -
100 - <script type="module">
101 - import { store } from "/components/plugins/plugin-settings-store.js";
102 - </script>
103 -</body>
104 -</html>