lvwerra HF Staff Claude Opus 4.6 commited on
Commit
d22e6fd
·
1 Parent(s): dcbdd12

Centralize agent type registry into single source of truth

Browse files

Replace ~17 scattered type definitions across 4 files with a unified
AGENT_REGISTRY on both backend (agents.py) and frontend (script.js).
Adding a new agent now only requires one registry entry per side.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (5) hide show
  1. backend/agents.py +350 -0
  2. backend/command.py +8 -97
  3. backend/main.py +11 -140
  4. frontend/index.html +6 -24
  5. frontend/script.js +110 -82
backend/agents.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Centralized Agent Type Registry.
3
+
4
+ Adding a new agent type:
5
+ 1. Add an entry to AGENT_REGISTRY below
6
+ 2. Implement its streaming handler (or use "builtin:chat" for simple LLM proxy)
7
+ 3. Add the same entry to AGENT_REGISTRY in frontend/script.js
8
+
9
+ SSE Event Protocol — every handler emits `data: {JSON}\\n\\n` lines with a `type` field.
10
+
11
+ Required events (all agents):
12
+ - done : stream complete
13
+ - error : { content: str } error message
14
+
15
+ Common optional events:
16
+ - thinking : { content: str } reasoning text
17
+ - content : { content: str } streamed response tokens
18
+ - result_preview: { content: str, figures: dict } inline result with optional images
19
+ - result : { content: str, figures: dict } final output for command center widget
20
+ - generating : still working between turns
21
+ - retry : { attempt, max_attempts, delay, message } retrying after error
22
+
23
+ Code-specific events:
24
+ - code_start : { code: str } before code execution
25
+ - code : { output, error, images } code cell result
26
+ - upload : { paths, output } files uploaded to sandbox
27
+ - download : { paths, output } files downloaded from sandbox
28
+
29
+ Research-specific events:
30
+ - status : { message } progress text
31
+ - queries : { queries: list, iteration: int } search queries generated
32
+ - source : { url, title, query_index, ... } source found
33
+ - query_stats : { query_index, relevant_count, irrelevant_count, error_count }
34
+ - assessment : { sufficient, missing_aspects, findings_count, reasoning }
35
+ """
36
+
37
+ # ============================================================
38
+ # THE REGISTRY — single source of truth for all agent types
39
+ # ============================================================
40
+
41
+ AGENT_REGISTRY = {
42
+ "command": {
43
+ "label": "TASKS",
44
+ "system_prompt": (
45
+ "You are a helpful AI assistant in the Productive interface command center.\n\n"
46
+ "{tools_section}\n\n"
47
+ "When a user asks you to perform a task that would benefit from a specialized notebook, you can:\n"
48
+ "1. Briefly acknowledge the request\n"
49
+ "2. Use the appropriate tool to launch a notebook with the task\n\n"
50
+ "You can also answer questions directly without launching a notebook if appropriate.\n\n"
51
+ "Examples:\n"
52
+ '- User: "Can you help me analyze this CSV file?"\n'
53
+ " You: Use launch_code_notebook tool with the task\n\n"
54
+ '- User: "Research the latest developments in AI"\n'
55
+ " You: Use launch_research_notebook tool with the topic\n\n"
56
+ '- User: "What was the result of the research in 2 sentences?"\n'
57
+ " You: Summarize the research results without using tools\n\n"
58
+ "Be concise and helpful. Don't duplicate effort - either answer directly OR launch a notebook, not both. "
59
+ "Answer questions about results directly without launching new notebooks.\n\n"
60
+ "IMPORTANT guidelines when delegating to notebooks:\n"
61
+ "- Do NOT ask notebooks to save or create files unless the user explicitly requests it or implicitly necessary to solve a task.\n"
62
+ "- NEVER overwrite existing files without explicit user permission.\n"
63
+ "- Each notebook has a task_id. If a a new task is clearly related to a existing notebook "
64
+ "re-use the task id to reuse the notebook. This will reuse the existing context and also the jupyter kernel for code notebooks."
65
+ ),
66
+ "tool": None,
67
+ "tool_arg": None,
68
+ "has_counter": False,
69
+ "in_menu": False,
70
+ "in_launcher": False,
71
+ "placeholder": "Enter message...",
72
+ },
73
+
74
+ "agent": {
75
+ "label": "AGENT",
76
+ "system_prompt": (
77
+ "You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.\n\n"
78
+ "Your role is to:\n"
79
+ "- Understand complex tasks and break them down into clear steps\n"
80
+ "- Execute tasks methodically\n"
81
+ "- Keep track of progress and next steps\n"
82
+ "- Provide clear status updates\n\n"
83
+ "Focus on being proactive, organized, and thorough in completing multi-step workflows.\n"
84
+ ),
85
+ "tool": {
86
+ "type": "function",
87
+ "function": {
88
+ "name": "launch_agent_notebook",
89
+ "description": "Launch an autonomous agent notebook for multi-step tasks that need planning and execution. Use this for complex workflows, task organization, or anything requiring multiple coordinated steps.",
90
+ "parameters": {
91
+ "type": "object",
92
+ "properties": {
93
+ "task": {
94
+ "type": "string",
95
+ "description": "The task or instruction for the agent. Should contain all necessary context."
96
+ },
97
+ "task_id": {
98
+ "type": "string",
99
+ "description": "A 2-3 word summary of the task, separated by dashes."
100
+ }
101
+ },
102
+ "required": ["task", "task_id"]
103
+ }
104
+ }
105
+ },
106
+ "tool_arg": "task",
107
+ "has_counter": True,
108
+ "in_menu": False,
109
+ "in_launcher": True,
110
+ "placeholder": "Enter message...",
111
+ },
112
+
113
+ "code": {
114
+ "label": "CODE",
115
+ "system_prompt": (
116
+ "You are a coding assistant with access to a Python code execution environment.\n\n"
117
+ "Your role is to:\n"
118
+ "- Write and execute Python code to solve problems\n"
119
+ "- Analyze data and answer questions\n"
120
+ "- Debug code and explain errors\n"
121
+ "- Break down complex tasks into executable steps\n\n"
122
+ "You have access to a Jupyter kernel with these packages:\n"
123
+ "pandas, matplotlib, numpy, scipy, scikit-learn, seaborn, plotly, requests, beautifulsoup4, and more.\n\n"
124
+ "You have three tools available:\n"
125
+ "- execute_code: Run Python code. The execution environment is stateful - variables and imports persist between calls.\n"
126
+ "- upload_files: Upload files from the workspace to the execution environment for analysis. "
127
+ "Files will be available at /home/user/<filename>. Use this when you need to analyze data files, scripts, or other files from the project.\n"
128
+ "- download_files: Download files from the execution environment to the workspace. "
129
+ "ONLY use this when the user explicitly asks to save/download files, or when saving files is clearly part of the task "
130
+ '(e.g., "generate a dataset and save it"). Do NOT automatically save intermediate files, plots, or outputs unless requested.\n\n'
131
+ "## IMPORTANT Guidelines\n\n"
132
+ "**Only create artifacts when explicitly requested:**\n"
133
+ "- Do NOT save files unless the user explicitly asks to save/export/download\n"
134
+ "- NEVER overwrite existing files without explicit user permission - always ask first or use a new filename\n\n"
135
+ "When solving problems:\n"
136
+ "1. Break down the task into logical steps\n"
137
+ "2. Execute code incrementally\n"
138
+ "3. Check outputs before proceeding\n"
139
+ "4. Only create visualizations if explicitly requested\n\n"
140
+ "IMPORTANT: When you DO generate plots/figures (only when requested), they are automatically named as figure_1, figure_2, etc. "
141
+ 'The execution output will show which figures were created (e.g., "[Generated figures: figure_1, figure_2]").\n\n'
142
+ "## CRITICAL: You MUST provide a <result> tag\n\n"
143
+ "When you have completed the task, you MUST provide a brief summary using the <result> tag. "
144
+ "This is REQUIRED - without it, your work will not be visible in the command center.\n\n"
145
+ "Keep results SHORT - 1-2 sentences max. Do NOT list implementation details, styling choices, or technical specifics. "
146
+ "The user can see the code and output above. Just state what was done.\n\n"
147
+ "To include figures, use self-closing tags like <figure_1> (NOT </figure_1> or <figure_1></figure_1>).\n\n"
148
+ "Example:\n"
149
+ "<result>\n"
150
+ "Here's the sine function plot:\n\n"
151
+ "<figure_1>\n"
152
+ "</result>\n"
153
+ ),
154
+ "tool": {
155
+ "type": "function",
156
+ "function": {
157
+ "name": "launch_code_notebook",
158
+ "description": "Launch a code notebook with Python execution environment. Use this for data analysis, creating visualizations, running code, debugging, or anything involving programming.",
159
+ "parameters": {
160
+ "type": "object",
161
+ "properties": {
162
+ "task": {
163
+ "type": "string",
164
+ "description": "The coding task or question. Should contain all necessary context."
165
+ },
166
+ "task_id": {
167
+ "type": "string",
168
+ "description": "A 2-3 word summary of the task, separated by dashes."
169
+ }
170
+ },
171
+ "required": ["task", "task_id"]
172
+ }
173
+ }
174
+ },
175
+ "tool_arg": "task",
176
+ "has_counter": True,
177
+ "in_menu": True,
178
+ "in_launcher": True,
179
+ "placeholder": "Enter message...",
180
+ },
181
+
182
+ "research": {
183
+ "label": "RESEARCH",
184
+ "system_prompt": (
185
+ "You are a research assistant specialized in deep analysis and information gathering.\n\n"
186
+ "Your role is to:\n"
187
+ "- Conduct thorough research on topics\n"
188
+ "- Synthesize information from multiple sources\n"
189
+ "- Provide well-structured, evidence-based answers\n"
190
+ "- Identify key insights and trends\n\n"
191
+ "When presenting your final research report:\n"
192
+ "1. Be CONCISE - focus on key findings, not lengthy explanations\n"
193
+ "2. Use TABLES wherever possible to structure information clearly\n"
194
+ "3. Use markdown table syntax for comparisons, lists of facts, statistics, etc.\n"
195
+ "4. Example table format:\n"
196
+ " | Category | Details |\n"
197
+ " |----------|--------|\n"
198
+ " | Item 1 | Data |\n"
199
+ " | Item 2 | Data |\n\n"
200
+ "5. Only use prose for context and synthesis that can't be tabulated\n"
201
+ '6. DO NOT include a title or heading like "Research Report:" - start directly with your findings\n\n'
202
+ "When you have completed your research, wrap your final report in <result> tags:\n\n"
203
+ "<result>\n"
204
+ "Your concise, table-based report here (NO title/heading)\n"
205
+ "</result>\n\n"
206
+ "The report will be sent back to the main interface.\n\n"
207
+ "Focus on being comprehensive, analytical, and well-sourced in your research.\n"
208
+ ),
209
+ "tool": {
210
+ "type": "function",
211
+ "function": {
212
+ "name": "launch_research_notebook",
213
+ "description": "Launch a research notebook for deep analysis requiring web search. Use this for researching topics, gathering information from multiple sources, or analyzing current information.",
214
+ "parameters": {
215
+ "type": "object",
216
+ "properties": {
217
+ "topic": {
218
+ "type": "string",
219
+ "description": "The research topic or question. Should be clear and specific."
220
+ },
221
+ "task_id": {
222
+ "type": "string",
223
+ "description": "A 2-3 word summary of the research topic, separated by dashes."
224
+ }
225
+ },
226
+ "required": ["topic", "task_id"]
227
+ }
228
+ }
229
+ },
230
+ "tool_arg": "topic",
231
+ "has_counter": True,
232
+ "in_menu": True,
233
+ "in_launcher": True,
234
+ "placeholder": "Enter message...",
235
+ },
236
+
237
+ "chat": {
238
+ "label": "CHAT",
239
+ "system_prompt": (
240
+ "You are a conversational AI assistant.\n\n"
241
+ "Your role is to:\n"
242
+ "- Engage in natural, helpful conversation\n"
243
+ "- Answer questions clearly and concisely\n"
244
+ "- Provide thoughtful responses\n"
245
+ "- Be friendly and approachable\n\n"
246
+ "Focus on being conversational, helpful, and easy to understand.\n"
247
+ ),
248
+ "tool": {
249
+ "type": "function",
250
+ "function": {
251
+ "name": "launch_chat_notebook",
252
+ "description": "Launch a conversational chat notebook for extended back-and-forth discussion. Use this when the user wants to continue a conversation in a dedicated space.",
253
+ "parameters": {
254
+ "type": "object",
255
+ "properties": {
256
+ "message": {
257
+ "type": "string",
258
+ "description": "The initial message or context for the chat."
259
+ },
260
+ "task_id": {
261
+ "type": "string",
262
+ "description": "A 2-3 word summary of the conversation topic, separated by dashes."
263
+ }
264
+ },
265
+ "required": ["message", "task_id"]
266
+ }
267
+ }
268
+ },
269
+ "tool_arg": "message",
270
+ "has_counter": True,
271
+ "in_menu": True,
272
+ "in_launcher": True,
273
+ "placeholder": "Enter message...",
274
+ },
275
+ }
276
+
277
+
278
+ # ============================================================
279
+ # Derived helpers — replace scattered dicts across the codebase
280
+ # ============================================================
281
+
282
+ def get_system_prompt(agent_key: str) -> str:
283
+ """Get system prompt for an agent type."""
284
+ agent = AGENT_REGISTRY.get(agent_key)
285
+ if not agent:
286
+ return ""
287
+ prompt = agent["system_prompt"]
288
+ # For command center, fill in the tools section dynamically
289
+ if "{tools_section}" in prompt:
290
+ prompt = prompt.replace("{tools_section}", _build_tools_section())
291
+ return prompt
292
+
293
+
294
+ def get_tools() -> list:
295
+ """Get tool definitions for the command center (replaces TOOLS in command.py)."""
296
+ return [
297
+ agent["tool"]
298
+ for agent in AGENT_REGISTRY.values()
299
+ if agent["tool"] is not None
300
+ ]
301
+
302
+
303
+ def get_notebook_type_map() -> dict:
304
+ """Map tool function names to agent keys (replaces notebook_type_map in command.py)."""
305
+ result = {}
306
+ for key, agent in AGENT_REGISTRY.items():
307
+ if agent["tool"] is not None:
308
+ func_name = agent["tool"]["function"]["name"]
309
+ result[func_name] = key
310
+ return result
311
+
312
+
313
+ def get_tool_arg(agent_key: str) -> str:
314
+ """Get the argument name for extracting the initial message from tool call args."""
315
+ agent = AGENT_REGISTRY.get(agent_key)
316
+ return agent["tool_arg"] if agent else "task"
317
+
318
+
319
+ def get_default_counters() -> dict:
320
+ """Get default notebook counters (replaces hardcoded dict in get_default_workspace)."""
321
+ return {
322
+ key: 0
323
+ for key, agent in AGENT_REGISTRY.items()
324
+ if agent["has_counter"]
325
+ }
326
+
327
+
328
+ def get_registry_for_frontend() -> list:
329
+ """Serialize registry metadata for the frontend /api/agents endpoint."""
330
+ return [
331
+ {
332
+ "key": key,
333
+ "label": agent["label"],
334
+ "hasCounter": agent["has_counter"],
335
+ "inMenu": agent["in_menu"],
336
+ "inLauncher": agent["in_launcher"],
337
+ "placeholder": agent["placeholder"],
338
+ }
339
+ for key, agent in AGENT_REGISTRY.items()
340
+ ]
341
+
342
+
343
+ def _build_tools_section() -> str:
344
+ """Build the 'available tools' text for the command center system prompt."""
345
+ lines = ["You have access to tools that can launch specialized notebooks for different types of tasks:"]
346
+ for key, agent in AGENT_REGISTRY.items():
347
+ if agent["tool"] is not None:
348
+ tool_func = agent["tool"]["function"]
349
+ lines.append(f"- {tool_func['name']}: {tool_func['description']}")
350
+ return "\n".join(lines)
backend/command.py CHANGED
@@ -9,93 +9,10 @@ from typing import List, Dict
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
- # Tool definitions for launching notebooks
13
- TOOLS = [
14
- {
15
- "type": "function",
16
- "function": {
17
- "name": "launch_agent_notebook",
18
- "description": "Launch an autonomous agent notebook for multi-step tasks that need planning and execution. Use this for complex workflows, task organization, or anything requiring multiple coordinated steps.",
19
- "parameters": {
20
- "type": "object",
21
- "properties": {
22
- "task": {
23
- "type": "string",
24
- "description": "The task or instruction for the agent. Should contain all necessary context."
25
- },
26
- "task_id": {
27
- "type": "string",
28
- "description": "A 2-3 word summary of the task, separated by dashes."
29
- }
30
- },
31
- "required": ["task", "task_id"]
32
- }
33
- }
34
- },
35
- {
36
- "type": "function",
37
- "function": {
38
- "name": "launch_code_notebook",
39
- "description": "Launch a code notebook with Python execution environment. Use this for data analysis, creating visualizations, running code, debugging, or anything involving programming.",
40
- "parameters": {
41
- "type": "object",
42
- "properties": {
43
- "task": {
44
- "type": "string",
45
- "description": "The coding task or question. Should contain all necessary context."
46
- },
47
- "task_id": {
48
- "type": "string",
49
- "description": "A 2-3 word summary of the task, separated by dashes."
50
- }
51
- },
52
- "required": ["task", "task_id"]
53
- }
54
- }
55
- },
56
- {
57
- "type": "function",
58
- "function": {
59
- "name": "launch_research_notebook",
60
- "description": "Launch a research notebook for deep analysis requiring web search. Use this for researching topics, gathering information from multiple sources, or analyzing current information.",
61
- "parameters": {
62
- "type": "object",
63
- "properties": {
64
- "topic": {
65
- "type": "string",
66
- "description": "The research topic or question. Should be clear and specific."
67
- },
68
- "task_id": {
69
- "type": "string",
70
- "description": "A 2-3 word summary of the research topic, separated by dashes."
71
- }
72
- },
73
- "required": ["topic", "task_id"]
74
- }
75
- }
76
- },
77
- {
78
- "type": "function",
79
- "function": {
80
- "name": "launch_chat_notebook",
81
- "description": "Launch a conversational chat notebook for extended back-and-forth discussion. Use this when the user wants to continue a conversation in a dedicated space.",
82
- "parameters": {
83
- "type": "object",
84
- "properties": {
85
- "message": {
86
- "type": "string",
87
- "description": "The initial message or context for the chat."
88
- },
89
- "task_id": {
90
- "type": "string",
91
- "description": "A 2-3 word summary of the conversation topic, separated by dashes."
92
- }
93
- },
94
- "required": ["message", "task_id"]
95
- }
96
- }
97
- }
98
- ]
99
 
100
  MAX_TURNS = 10 # Limit conversation turns in command center
101
  MAX_RETRIES = 3 # Maximum retries for LLM calls
@@ -219,19 +136,13 @@ def stream_command_center(client, model: str, messages: List[Dict], extra_params
219
  yield {"type": "error", "content": "Failed to parse tool arguments"}
220
  return
221
 
222
- # Map function names to notebook types
223
- notebook_type_map = {
224
- "launch_agent_notebook": "agent",
225
- "launch_code_notebook": "code",
226
- "launch_research_notebook": "research",
227
- "launch_chat_notebook": "chat"
228
- }
229
-
230
  notebook_type = notebook_type_map.get(function_name)
231
 
232
  if notebook_type:
233
- # Get the message/task/topic from args
234
- initial_message = args.get("task") or args.get("topic") or args.get("message")
235
  task_id = args.get("task_id", "")
236
 
237
  # Send launch action to frontend
 
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
+ # Tool definitions derived from agent registry
13
+ from agents import get_tools, get_notebook_type_map, get_tool_arg
14
+
15
+ TOOLS = get_tools()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  MAX_TURNS = 10 # Limit conversation turns in command center
18
  MAX_RETRIES = 3 # Maximum retries for LLM calls
 
136
  yield {"type": "error", "content": "Failed to parse tool arguments"}
137
  return
138
 
139
+ # Map function names to notebook types (derived from registry)
140
+ notebook_type_map = get_notebook_type_map()
 
 
 
 
 
 
141
  notebook_type = notebook_type_map.get(function_name)
142
 
143
  if notebook_type:
144
+ # Get the initial message using the registered arg name for this type
145
+ initial_message = args.get(get_tool_arg(notebook_type)) or args.get("task") or args.get("message")
146
  task_id = args.get("task_id", "")
147
 
148
  # Send launch action to frontend
backend/main.py CHANGED
@@ -13,6 +13,7 @@ import signal
13
  import sys
14
  from datetime import datetime
15
  from concurrent.futures import ThreadPoolExecutor
 
16
 
17
  # Thread pool for running sync generators without blocking the event loop
18
  # Use daemon threads so they don't block shutdown
@@ -154,139 +155,7 @@ app.add_middleware(
154
  allow_headers=["*"],
155
  )
156
 
157
- # System prompts for each notebook type
158
- SYSTEM_PROMPTS = {
159
- "command": """You are a helpful AI assistant in the Productive interface command center.
160
-
161
- You have access to tools that can launch specialized notebooks for different types of tasks:
162
- - launch_agent_notebook: For multi-step tasks that need planning and execution
163
- - launch_code_notebook: For data analysis, visualizations, running Python code
164
- - launch_research_notebook: For researching topics using web search
165
- - launch_chat_notebook: For extended conversational discussions
166
-
167
- When a user asks you to perform a task that would benefit from a specialized notebook, you can:
168
- 1. Briefly acknowledge the request
169
- 2. Use the appropriate tool to launch a notebook with the task
170
-
171
- You can also answer questions directly without launching a notebook if appropriate.
172
-
173
- Examples:
174
- - User: "Can you help me analyze this CSV file?"
175
- You: Use launch_code_notebook tool with the task
176
-
177
- - User: "Research the latest developments in AI"
178
- You: Use launch_research_notebook tool with the topic
179
-
180
- - User: "What was the result of the research in 2 sentences?"
181
- You: Summarize the research results without using tools
182
-
183
- Be concise and helpful. Don't duplicate effort - either answer directly OR launch a notebook, not both. Answer questions about results directly without launching new notebooks.
184
-
185
- IMPORTANT guidelines when delegating to notebooks:
186
- - Do NOT ask notebooks to save or create files unless the user explicitly requests it or implicitly necessary to solve a task.
187
- - NEVER overwrite existing files without explicit user permission.
188
- - Each notebook has a task_id. If a a new task is clearly related to a existing notebook re-use the task id to reuse the notebook. This will reuse the existing context and also the jupyter kernel for code notebooks.""",
189
- "agent": """You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.
190
-
191
- Your role is to:
192
- - Understand complex tasks and break them down into clear steps
193
- - Execute tasks methodically
194
- - Keep track of progress and next steps
195
- - Provide clear status updates
196
-
197
- Focus on being proactive, organized, and thorough in completing multi-step workflows.
198
- """,
199
- "code": """You are a coding assistant with access to a Python code execution environment.
200
-
201
- Your role is to:
202
- - Write and execute Python code to solve problems
203
- - Analyze data and answer questions
204
- - Debug code and explain errors
205
- - Break down complex tasks into executable steps
206
-
207
- You have access to a Jupyter kernel with these packages:
208
- pandas, matplotlib, numpy, scipy, scikit-learn, seaborn, plotly, requests, beautifulsoup4, and more.
209
-
210
- You have three tools available:
211
- - execute_code: Run Python code. The execution environment is stateful - variables and imports persist between calls.
212
- - upload_files: Upload files from the workspace to the execution environment for analysis. Files will be available at /home/user/<filename>. Use this when you need to analyze data files, scripts, or other files from the project.
213
- - download_files: Download files from the execution environment to the workspace. ONLY use this when the user explicitly asks to save/download files, or when saving files is clearly part of the task (e.g., "generate a dataset and save it"). Do NOT automatically save intermediate files, plots, or outputs unless requested.
214
-
215
- ## IMPORTANT Guidelines
216
-
217
- **Only create artifacts when explicitly requested:**
218
- - Do NOT save files unless the user explicitly asks to save/export/download
219
- - NEVER overwrite existing files without explicit user permission - always ask first or use a new filename
220
-
221
- When solving problems:
222
- 1. Break down the task into logical steps
223
- 2. Execute code incrementally
224
- 3. Check outputs before proceeding
225
- 4. Only create visualizations if explicitly requested
226
-
227
- IMPORTANT: When you DO generate plots/figures (only when requested), they are automatically named as figure_1, figure_2, etc. The execution output will show which figures were created (e.g., "[Generated figures: figure_1, figure_2]").
228
-
229
- ## CRITICAL: You MUST provide a <result> tag
230
-
231
- When you have completed the task, you MUST ALWAYS provide a summary using the <result> tag. This is REQUIRED - without it, your work will not be visible in the command center. To include figures in your result (when you've created them), use self-closing figure tags like <figure_1>, <figure_2>, <figure_3> etc.:
232
-
233
- Example:
234
- <result>
235
- Created a sine and cosine plot from 0 to 2π:
236
-
237
- <figure_1>
238
-
239
- The plot shows both functions overlaid on the same axes.
240
- </result>
241
-
242
- IMPORTANT: Use self-closing tags like <figure_1> (NOT </figure_1> or <figure_1></figure_1>). Each tag will be replaced with the actual image.
243
-
244
- The result will be sent back to the command center with embedded images. DO NOT forget the <result> tag - it is mandatory for every completed task.
245
-
246
- Focus on being precise, practical, and thorough in your coding assistance.
247
- """,
248
- "research": """You are a research assistant specialized in deep analysis and information gathering.
249
-
250
- Your role is to:
251
- - Conduct thorough research on topics
252
- - Synthesize information from multiple sources
253
- - Provide well-structured, evidence-based answers
254
- - Identify key insights and trends
255
-
256
- When presenting your final research report:
257
- 1. Be CONCISE - focus on key findings, not lengthy explanations
258
- 2. Use TABLES wherever possible to structure information clearly
259
- 3. Use markdown table syntax for comparisons, lists of facts, statistics, etc.
260
- 4. Example table format:
261
- | Category | Details |
262
- |----------|---------|
263
- | Item 1 | Data |
264
- | Item 2 | Data |
265
-
266
- 5. Only use prose for context and synthesis that can't be tabulated
267
- 6. DO NOT include a title or heading like "Research Report:" - start directly with your findings
268
-
269
- When you have completed your research, wrap your final report in <result> tags:
270
-
271
- <result>
272
- Your concise, table-based report here (NO title/heading)
273
- </result>
274
-
275
- The report will be sent back to the main interface.
276
-
277
- Focus on being comprehensive, analytical, and well-sourced in your research.
278
- """,
279
- "chat": """You are a conversational AI assistant.
280
-
281
- Your role is to:
282
- - Engage in natural, helpful conversation
283
- - Answer questions clearly and concisely
284
- - Provide thoughtful responses
285
- - Be friendly and approachable
286
-
287
- Focus on being conversational, helpful, and easy to understand.
288
- """
289
- }
290
 
291
 
292
  def record_api_call(tab_id: str, messages: List[dict]):
@@ -737,6 +606,12 @@ async def api_info():
737
  }
738
 
739
 
 
 
 
 
 
 
740
  @app.post("/api/generate-title")
741
  async def generate_title(request: TitleRequest):
742
  """Generate a short 2-3 word title for a user query"""
@@ -1275,12 +1150,7 @@ def get_default_workspace():
1275
  "version": 1,
1276
  "tabCounter": 1,
1277
  "activeTabId": 0,
1278
- "notebookCounters": {
1279
- "agent": 0,
1280
- "code": 0,
1281
- "research": 0,
1282
- "chat": 0
1283
- },
1284
  "tabs": [
1285
  {
1286
  "id": 0,
@@ -1437,7 +1307,8 @@ Current theme: {name}
1437
 
1438
  def get_system_prompt(notebook_type: str, frontend_context: Optional[Dict] = None) -> str:
1439
  """Get system prompt for a notebook type with dynamic context appended"""
1440
- base_prompt = SYSTEM_PROMPTS.get(notebook_type, SYSTEM_PROMPTS["command"])
 
1441
  file_tree = get_file_tree_for_prompt()
1442
 
1443
  # Build the full prompt with context sections
 
13
  import sys
14
  from datetime import datetime
15
  from concurrent.futures import ThreadPoolExecutor
16
+ from agents import AGENT_REGISTRY, get_default_counters, get_registry_for_frontend
17
 
18
  # Thread pool for running sync generators without blocking the event loop
19
  # Use daemon threads so they don't block shutdown
 
155
  allow_headers=["*"],
156
  )
157
 
158
+ # Agent type registry is in agents.py — system prompts, tools, and metadata are all defined there
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
 
161
  def record_api_call(tab_id: str, messages: List[dict]):
 
606
  }
607
 
608
 
609
+ @app.get("/api/agents")
610
+ async def get_agents():
611
+ """Return agent type registry for frontend consumption."""
612
+ return {"agents": get_registry_for_frontend()}
613
+
614
+
615
  @app.post("/api/generate-title")
616
  async def generate_title(request: TitleRequest):
617
  """Generate a short 2-3 word title for a user query"""
 
1150
  "version": 1,
1151
  "tabCounter": 1,
1152
  "activeTabId": 0,
1153
+ "notebookCounters": get_default_counters(),
 
 
 
 
 
1154
  "tabs": [
1155
  {
1156
  "id": 0,
 
1307
 
1308
  def get_system_prompt(notebook_type: str, frontend_context: Optional[Dict] = None) -> str:
1309
  """Get system prompt for a notebook type with dynamic context appended"""
1310
+ from agents import get_system_prompt as _get_agent_prompt
1311
+ base_prompt = _get_agent_prompt(notebook_type) or _get_agent_prompt("command")
1312
  file_tree = get_file_tree_for_prompt()
1313
 
1314
  # Build the full prompt with context sections
frontend/index.html CHANGED
@@ -20,9 +20,7 @@
20
  <div class="new-tab-wrapper">
21
  <button class="new-tab-btn" id="newTabBtn">+</button>
22
  <div class="new-tab-menu" id="newTabMenu">
23
- <div class="menu-item" data-type="chat">BASE</div>
24
- <div class="menu-item" data-type="code">CODE</div>
25
- <div class="menu-item" data-type="research">RESEARCH</div>
26
  </div>
27
  </div>
28
  <div class="tab-bar-spacer"></div>
@@ -60,11 +58,8 @@
60
  <div class="notebook-type">TASK CENTER</div>
61
  <h2>Task Center</h2>
62
  </div>
63
- <div class="header-actions">
64
- <button class="launcher-btn" data-type="agent">AGENT</button>
65
- <button class="launcher-btn" data-type="code">CODE</button>
66
- <button class="launcher-btn" data-type="research">RESEARCH</button>
67
- <button class="launcher-btn" data-type="chat">CHAT</button>
68
  <button class="debug-btn" id="debugBtn">DEBUG</button>
69
  </div>
70
  </div>
@@ -210,21 +205,8 @@
210
  <span class="label-text">NOTEBOOK MODELS</span>
211
  <span class="label-description">Select which model to use for each notebook type</span>
212
  </label>
213
- <div class="notebook-models-grid">
214
- <label>COMMAND:</label>
215
- <select id="setting-notebook-command" class="settings-select"></select>
216
-
217
- <label>AGENT:</label>
218
- <select id="setting-notebook-agent" class="settings-select"></select>
219
-
220
- <label>CODE:</label>
221
- <select id="setting-notebook-code" class="settings-select"></select>
222
-
223
- <label>RESEARCH:</label>
224
- <select id="setting-notebook-research" class="settings-select"></select>
225
-
226
- <label>CHAT:</label>
227
- <select id="setting-notebook-chat" class="settings-select"></select>
228
  </div>
229
  </div>
230
 
@@ -475,6 +457,6 @@
475
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
476
  <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
477
  <script src="research-ui.js?v=23"></script>
478
- <script src="script.js?v=56"></script>
479
  </body>
480
  </html>
 
20
  <div class="new-tab-wrapper">
21
  <button class="new-tab-btn" id="newTabBtn">+</button>
22
  <div class="new-tab-menu" id="newTabMenu">
23
+ <!-- Generated dynamically from AGENT_REGISTRY -->
 
 
24
  </div>
25
  </div>
26
  <div class="tab-bar-spacer"></div>
 
58
  <div class="notebook-type">TASK CENTER</div>
59
  <h2>Task Center</h2>
60
  </div>
61
+ <div class="header-actions" id="launcherButtons">
62
+ <!-- Launcher buttons generated dynamically from AGENT_REGISTRY -->
 
 
 
63
  <button class="debug-btn" id="debugBtn">DEBUG</button>
64
  </div>
65
  </div>
 
205
  <span class="label-text">NOTEBOOK MODELS</span>
206
  <span class="label-description">Select which model to use for each notebook type</span>
207
  </label>
208
+ <div class="notebook-models-grid" id="notebookModelsGrid">
209
+ <!-- Generated dynamically from AGENT_REGISTRY -->
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  </div>
211
  </div>
212
 
 
457
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
458
  <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
459
  <script src="research-ui.js?v=23"></script>
460
+ <script src="script.js?v=57"></script>
461
  </body>
462
  </html>
frontend/script.js CHANGED
@@ -1,3 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // State management
2
  let tabCounter = 1;
3
  let activeTabId = 0;
@@ -22,13 +53,7 @@ let settings = {
22
  // New provider/model structure
23
  providers: {}, // providerId -> {name, endpoint, token}
24
  models: {}, // modelId -> {name, providerId, modelId (API model string)}
25
- notebooks: { // notebook type -> modelId
26
- command: '',
27
- agent: '',
28
- code: '',
29
- research: '',
30
- chat: ''
31
- },
32
  // Service API keys
33
  e2bKey: '',
34
  serperKey: '',
@@ -51,21 +76,8 @@ const toolCallIds = {};
51
  // Track notebooks by task_id for reuse (maps task_id -> tabId)
52
  const taskIdToTabId = {};
53
 
54
- // Track notebook counters for each type
55
- let notebookCounters = {
56
- 'agent': 0,
57
- 'code': 0,
58
- 'research': 0,
59
- 'chat': 0
60
- };
61
-
62
- const notebookTitles = {
63
- 'command-center': 'Task Center',
64
- 'agent': 'AGENT',
65
- 'code': 'CODE',
66
- 'research': 'RESEARCH',
67
- 'chat': 'CHAT'
68
- };
69
 
70
  // Debounce timer for workspace saving
71
  let saveWorkspaceTimer = null;
@@ -90,7 +102,7 @@ function resetLocalState() {
90
  Object.keys(taskIdToTabId).forEach(k => delete taskIdToTabId[k]);
91
  researchQueryTabIds = {};
92
  showAllTurns = false;
93
- notebookCounters = { agent: 0, code: 0, research: 0, chat: 0 };
94
 
95
  // Reset timeline data
96
  Object.keys(timelineData).forEach(k => delete timelineData[k]);
@@ -362,8 +374,7 @@ function renderTimeline() {
362
  function renderNotebookTimeline(tabId, notebook, isNested = false) {
363
  const isActive = activeTabId === tabId;
364
  const isClosed = notebook.isClosed || false;
365
- const typeLabels = { 'agent': 'AGENT', 'code': 'CODE', 'research': 'RESEARCH', 'chat': 'CHAT', 'command': 'TASKS', 'search': 'SEARCH', 'browse': 'BROWSE' };
366
- const typeLabel = typeLabels[notebook.type] || notebook.type.toUpperCase();
367
 
368
  let html = `<div class="tl-widget${isNested ? '' : ' compact'}${isActive ? ' active' : ''}${isClosed ? ' closed' : ''}" data-tab-id="${tabId}">`;
369
 
@@ -423,7 +434,7 @@ function renderNotebookTimeline(tabId, notebook, isNested = false) {
423
  if (event.childTabId !== null) {
424
  const childNotebook = timelineData[event.childTabId];
425
  if (childNotebook) {
426
- const childTypeLabel = typeLabels[childNotebook.type] || childNotebook.type.toUpperCase();
427
  const childIsGenerating = childNotebook.isGenerating;
428
  const turnCount = childNotebook.events.length;
429
 
@@ -590,7 +601,60 @@ document.addEventListener('DOMContentLoaded', async () => {
590
  showSessionSelector(sessionsData.sessions);
591
  });
592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  function initializeEventListeners() {
 
 
 
 
 
594
  // Launcher buttons in command center
595
  document.querySelectorAll('.launcher-btn').forEach(btn => {
596
  btn.addEventListener('click', (e) => {
@@ -1123,7 +1187,7 @@ function createNotebookTab(type, initialMessage = null, autoSwitch = true, taskI
1123
  notebookCounters[type]++;
1124
  title = `New ${type} task`;
1125
  } else {
1126
- title = notebookTitles[type];
1127
  }
1128
 
1129
  // Register in timeline
@@ -1205,20 +1269,6 @@ function createNotebookContent(type, tabId, title = null) {
1205
  return document.querySelector('[data-content-id="0"]').innerHTML;
1206
  }
1207
 
1208
- const placeholders = {
1209
- 'agent': 'Enter message...',
1210
- 'code': 'Enter message...',
1211
- 'research': 'Enter message...',
1212
- 'chat': 'Enter message...'
1213
- };
1214
-
1215
- const typeLabels = {
1216
- 'agent': 'AGENT',
1217
- 'code': 'CODE',
1218
- 'research': 'RESEARCH',
1219
- 'chat': 'CHAT'
1220
- };
1221
-
1222
  // Use unique ID combining type and tabId to ensure unique container IDs
1223
  const uniqueId = `${type}-${tabId}`;
1224
 
@@ -1229,7 +1279,7 @@ function createNotebookContent(type, tabId, title = null) {
1229
  <div class="notebook-interface">
1230
  <div class="notebook-header">
1231
  <div>
1232
- <div class="notebook-type">${typeLabels[type]}</div>
1233
  <h2>${escapeHtml(displayTitle)}</h2>
1234
  </div>
1235
  </div>
@@ -1239,7 +1289,7 @@ function createNotebookContent(type, tabId, title = null) {
1239
  </div>
1240
  <div class="input-area">
1241
  <div class="input-container">
1242
- <textarea placeholder="${placeholders[type]}" id="input-${uniqueId}" rows="1"></textarea>
1243
  <button>SEND</button>
1244
  </div>
1245
  </div>
@@ -2435,12 +2485,7 @@ async function loadWorkspace() {
2435
  function restoreWorkspace(workspace) {
2436
  // Restore counters
2437
  tabCounter = workspace.tabCounter || 1;
2438
- notebookCounters = workspace.notebookCounters || {
2439
- 'agent': 0,
2440
- 'code': 0,
2441
- 'research': 0,
2442
- 'chat': 0
2443
- };
2444
 
2445
  // Restore timeline data before tabs so renderTimeline works
2446
  if (workspace.timelineData) {
@@ -2477,7 +2522,7 @@ function restoreTab(tabData) {
2477
  tab.className = 'tab';
2478
  tab.dataset.tabId = tabData.id;
2479
  tab.innerHTML = `
2480
- <span class="tab-title">${tabData.title || notebookTitles[tabData.type] || 'TAB'}</span>
2481
  <span class="tab-status" style="display: none;"><span></span><span></span><span></span></span>
2482
  <span class="tab-close">×</span>
2483
  `;
@@ -2816,7 +2861,7 @@ function serializeTab(tabId, type) {
2816
  const tabData = {
2817
  id: tabId,
2818
  type: type,
2819
- title: tabEl?.querySelector('.tab-title')?.textContent || notebookTitles[type] || 'TAB',
2820
  messages: []
2821
  };
2822
 
@@ -3045,7 +3090,7 @@ function migrateSettings(oldSettings) {
3045
 
3046
  // Migrate notebook-specific models if they existed
3047
  const oldModels = oldSettings.models || {};
3048
- const notebookTypes = ['agent', 'code', 'research', 'chat'];
3049
  notebookTypes.forEach(type => {
3050
  if (oldModels[type]) {
3051
  const specificModelId = `model_${type}`;
@@ -3186,12 +3231,9 @@ function populateModelDropdowns() {
3186
  const models = settings.models || {};
3187
  const notebooks = settings.notebooks || {};
3188
 
 
3189
  const dropdownIds = [
3190
- 'setting-notebook-command',
3191
- 'setting-notebook-agent',
3192
- 'setting-notebook-code',
3193
- 'setting-notebook-research',
3194
- 'setting-notebook-chat',
3195
  'setting-research-sub-agent-model'
3196
  ];
3197
 
@@ -3218,19 +3260,12 @@ function populateModelDropdowns() {
3218
  }
3219
  });
3220
 
3221
- // Set values from settings
3222
- const commandDropdown = document.getElementById('setting-notebook-command');
3223
- const agentDropdown = document.getElementById('setting-notebook-agent');
3224
- const codeDropdown = document.getElementById('setting-notebook-code');
3225
- const researchDropdown = document.getElementById('setting-notebook-research');
3226
- const chatDropdown = document.getElementById('setting-notebook-chat');
3227
  const subAgentDropdown = document.getElementById('setting-research-sub-agent-model');
3228
-
3229
- if (commandDropdown) commandDropdown.value = notebooks.command || '';
3230
- if (agentDropdown) agentDropdown.value = notebooks.agent || '';
3231
- if (codeDropdown) codeDropdown.value = notebooks.code || '';
3232
- if (researchDropdown) researchDropdown.value = notebooks.research || '';
3233
- if (chatDropdown) chatDropdown.value = notebooks.chat || '';
3234
  if (subAgentDropdown) subAgentDropdown.value = settings.researchSubAgentModel || '';
3235
  }
3236
 
@@ -3433,12 +3468,11 @@ function openSettings() {
3433
  }
3434
 
3435
  async function saveSettings() {
3436
- // Get notebook model selections from dropdowns
3437
- const commandModel = document.getElementById('setting-notebook-command')?.value || '';
3438
- const agentModel = document.getElementById('setting-notebook-agent')?.value || '';
3439
- const codeModel = document.getElementById('setting-notebook-code')?.value || '';
3440
- const researchModel = document.getElementById('setting-notebook-research')?.value || '';
3441
- const chatModel = document.getElementById('setting-notebook-chat')?.value || '';
3442
  const researchSubAgentModel = document.getElementById('setting-research-sub-agent-model')?.value || '';
3443
 
3444
  // Get other settings
@@ -3460,13 +3494,7 @@ async function saveSettings() {
3460
  }
3461
 
3462
  // Update settings
3463
- settings.notebooks = {
3464
- command: commandModel,
3465
- agent: agentModel,
3466
- code: codeModel,
3467
- research: researchModel,
3468
- chat: chatModel
3469
- };
3470
  settings.e2bKey = e2bKey;
3471
  settings.serperKey = serperKey;
3472
  settings.researchSubAgentModel = researchSubAgentModel;
 
1
+ // ============================================================
2
+ // Agent Type Registry — single source of truth for the frontend
3
+ // To add a new agent type, add an entry here and in backend/agents.py
4
+ // ============================================================
5
+ const AGENT_REGISTRY = {
6
+ command: { label: 'TASKS', hasCounter: false, inMenu: false, inLauncher: false, placeholder: 'Enter message...' },
7
+ agent: { label: 'AGENT', hasCounter: true, inMenu: false, inLauncher: true, placeholder: 'Enter message...' },
8
+ code: { label: 'CODE', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Enter message...' },
9
+ research: { label: 'RESEARCH', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Enter message...' },
10
+ chat: { label: 'CHAT', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Enter message...' },
11
+ };
12
+ // Virtual types used only in timeline rendering (not real agents)
13
+ const VIRTUAL_TYPE_LABELS = { search: 'SEARCH', browse: 'BROWSE' };
14
+
15
+ // Derived helpers from registry
16
+ function getTypeLabel(type) {
17
+ if (AGENT_REGISTRY[type]) return AGENT_REGISTRY[type].label;
18
+ if (VIRTUAL_TYPE_LABELS[type]) return VIRTUAL_TYPE_LABELS[type];
19
+ return type.toUpperCase();
20
+ }
21
+ function getPlaceholder(type) {
22
+ return AGENT_REGISTRY[type]?.placeholder || 'Enter message...';
23
+ }
24
+ function getDefaultCounters() {
25
+ const counters = {};
26
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
27
+ if (agent.hasCounter) counters[key] = 0;
28
+ }
29
+ return counters;
30
+ }
31
+
32
  // State management
33
  let tabCounter = 1;
34
  let activeTabId = 0;
 
53
  // New provider/model structure
54
  providers: {}, // providerId -> {name, endpoint, token}
55
  models: {}, // modelId -> {name, providerId, modelId (API model string)}
56
+ notebooks: Object.fromEntries(Object.keys(AGENT_REGISTRY).map(k => [k, ''])),
 
 
 
 
 
 
57
  // Service API keys
58
  e2bKey: '',
59
  serperKey: '',
 
76
  // Track notebooks by task_id for reuse (maps task_id -> tabId)
77
  const taskIdToTabId = {};
78
 
79
+ // Track notebook counters for each type (derived from registry)
80
+ let notebookCounters = getDefaultCounters();
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  // Debounce timer for workspace saving
83
  let saveWorkspaceTimer = null;
 
102
  Object.keys(taskIdToTabId).forEach(k => delete taskIdToTabId[k]);
103
  researchQueryTabIds = {};
104
  showAllTurns = false;
105
+ notebookCounters = getDefaultCounters();
106
 
107
  // Reset timeline data
108
  Object.keys(timelineData).forEach(k => delete timelineData[k]);
 
374
  function renderNotebookTimeline(tabId, notebook, isNested = false) {
375
  const isActive = activeTabId === tabId;
376
  const isClosed = notebook.isClosed || false;
377
+ const typeLabel = getTypeLabel(notebook.type);
 
378
 
379
  let html = `<div class="tl-widget${isNested ? '' : ' compact'}${isActive ? ' active' : ''}${isClosed ? ' closed' : ''}" data-tab-id="${tabId}">`;
380
 
 
434
  if (event.childTabId !== null) {
435
  const childNotebook = timelineData[event.childTabId];
436
  if (childNotebook) {
437
+ const childTypeLabel = getTypeLabel(childNotebook.type);
438
  const childIsGenerating = childNotebook.isGenerating;
439
  const turnCount = childNotebook.events.length;
440
 
 
601
  showSessionSelector(sessionsData.sessions);
602
  });
603
 
604
+ // ============================================================
605
+ // Dynamic HTML generation from AGENT_REGISTRY
606
+ // ============================================================
607
+
608
+ function renderNewTabMenu() {
609
+ const menu = document.getElementById('newTabMenu');
610
+ if (!menu) return;
611
+ menu.innerHTML = '';
612
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
613
+ if (!agent.inMenu) continue;
614
+ const item = document.createElement('div');
615
+ item.className = 'menu-item';
616
+ item.dataset.type = key;
617
+ item.textContent = agent.label;
618
+ menu.appendChild(item);
619
+ }
620
+ }
621
+
622
+ function renderLauncherButtons() {
623
+ const container = document.getElementById('launcherButtons');
624
+ if (!container) return;
625
+ // Insert before the debug button (keep it at the end)
626
+ const debugBtn = container.querySelector('#debugBtn');
627
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
628
+ if (!agent.inLauncher) continue;
629
+ const btn = document.createElement('button');
630
+ btn.className = 'launcher-btn';
631
+ btn.dataset.type = key;
632
+ btn.textContent = agent.label;
633
+ container.insertBefore(btn, debugBtn);
634
+ }
635
+ }
636
+
637
+ function renderNotebookModelSelectors() {
638
+ const grid = document.getElementById('notebookModelsGrid');
639
+ if (!grid) return;
640
+ grid.innerHTML = '';
641
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
642
+ const label = document.createElement('label');
643
+ label.textContent = `${agent.label}:`;
644
+ const select = document.createElement('select');
645
+ select.id = `setting-notebook-${key}`;
646
+ select.className = 'settings-select';
647
+ grid.appendChild(label);
648
+ grid.appendChild(select);
649
+ }
650
+ }
651
+
652
  function initializeEventListeners() {
653
+ // Generate dynamic UI elements from registry
654
+ renderLauncherButtons();
655
+ renderNewTabMenu();
656
+ renderNotebookModelSelectors();
657
+
658
  // Launcher buttons in command center
659
  document.querySelectorAll('.launcher-btn').forEach(btn => {
660
  btn.addEventListener('click', (e) => {
 
1187
  notebookCounters[type]++;
1188
  title = `New ${type} task`;
1189
  } else {
1190
+ title = getTypeLabel(type);
1191
  }
1192
 
1193
  // Register in timeline
 
1269
  return document.querySelector('[data-content-id="0"]').innerHTML;
1270
  }
1271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1272
  // Use unique ID combining type and tabId to ensure unique container IDs
1273
  const uniqueId = `${type}-${tabId}`;
1274
 
 
1279
  <div class="notebook-interface">
1280
  <div class="notebook-header">
1281
  <div>
1282
+ <div class="notebook-type">${getTypeLabel(type)}</div>
1283
  <h2>${escapeHtml(displayTitle)}</h2>
1284
  </div>
1285
  </div>
 
1289
  </div>
1290
  <div class="input-area">
1291
  <div class="input-container">
1292
+ <textarea placeholder="${getPlaceholder(type)}" id="input-${uniqueId}" rows="1"></textarea>
1293
  <button>SEND</button>
1294
  </div>
1295
  </div>
 
2485
  function restoreWorkspace(workspace) {
2486
  // Restore counters
2487
  tabCounter = workspace.tabCounter || 1;
2488
+ notebookCounters = workspace.notebookCounters || getDefaultCounters();
 
 
 
 
 
2489
 
2490
  // Restore timeline data before tabs so renderTimeline works
2491
  if (workspace.timelineData) {
 
2522
  tab.className = 'tab';
2523
  tab.dataset.tabId = tabData.id;
2524
  tab.innerHTML = `
2525
+ <span class="tab-title">${tabData.title || getTypeLabel(tabData.type) || 'TAB'}</span>
2526
  <span class="tab-status" style="display: none;"><span></span><span></span><span></span></span>
2527
  <span class="tab-close">×</span>
2528
  `;
 
2861
  const tabData = {
2862
  id: tabId,
2863
  type: type,
2864
+ title: tabEl?.querySelector('.tab-title')?.textContent || getTypeLabel(type) || 'TAB',
2865
  messages: []
2866
  };
2867
 
 
3090
 
3091
  // Migrate notebook-specific models if they existed
3092
  const oldModels = oldSettings.models || {};
3093
+ const notebookTypes = Object.keys(AGENT_REGISTRY).filter(k => AGENT_REGISTRY[k].hasCounter);
3094
  notebookTypes.forEach(type => {
3095
  if (oldModels[type]) {
3096
  const specificModelId = `model_${type}`;
 
3231
  const models = settings.models || {};
3232
  const notebooks = settings.notebooks || {};
3233
 
3234
+ // Build dropdown IDs from registry + special dropdowns
3235
  const dropdownIds = [
3236
+ ...Object.keys(AGENT_REGISTRY).map(t => `setting-notebook-${t}`),
 
 
 
 
3237
  'setting-research-sub-agent-model'
3238
  ];
3239
 
 
3260
  }
3261
  });
3262
 
3263
+ // Set values from settings (driven by registry)
3264
+ for (const type of Object.keys(AGENT_REGISTRY)) {
3265
+ const dropdown = document.getElementById(`setting-notebook-${type}`);
3266
+ if (dropdown) dropdown.value = notebooks[type] || '';
3267
+ }
 
3268
  const subAgentDropdown = document.getElementById('setting-research-sub-agent-model');
 
 
 
 
 
 
3269
  if (subAgentDropdown) subAgentDropdown.value = settings.researchSubAgentModel || '';
3270
  }
3271
 
 
3468
  }
3469
 
3470
  async function saveSettings() {
3471
+ // Get notebook model selections from dropdowns (driven by registry)
3472
+ const notebookModels = {};
3473
+ for (const type of Object.keys(AGENT_REGISTRY)) {
3474
+ notebookModels[type] = document.getElementById(`setting-notebook-${type}`)?.value || '';
3475
+ }
 
3476
  const researchSubAgentModel = document.getElementById('setting-research-sub-agent-model')?.value || '';
3477
 
3478
  // Get other settings
 
3494
  }
3495
 
3496
  // Update settings
3497
+ settings.notebooks = notebookModels;
 
 
 
 
 
 
3498
  settings.e2bKey = e2bKey;
3499
  settings.serperKey = serperKey;
3500
  settings.researchSubAgentModel = researchSubAgentModel;