cowork-dash 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cowork_dash/__init__.py +35 -0
- cowork_dash/__main__.py +9 -0
- cowork_dash/agent.py +117 -0
- cowork_dash/app.py +1776 -0
- cowork_dash/assets/app.js +237 -0
- cowork_dash/assets/favicon.svg +1 -0
- cowork_dash/assets/styles.css +915 -0
- cowork_dash/canvas.py +318 -0
- cowork_dash/cli.py +273 -0
- cowork_dash/components.py +568 -0
- cowork_dash/config.py +91 -0
- cowork_dash/file_utils.py +226 -0
- cowork_dash/layout.py +250 -0
- cowork_dash/tools.py +699 -0
- cowork_dash-0.1.2.dist-info/METADATA +238 -0
- cowork_dash-0.1.2.dist-info/RECORD +19 -0
- cowork_dash-0.1.2.dist-info/WHEEL +4 -0
- cowork_dash-0.1.2.dist-info/entry_points.txt +2 -0
- cowork_dash-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""UI components for rendering messages, canvas items, and other UI elements."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
from dash import html, dcc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_message(role: str, content: str, colors: Dict, styles: Dict, is_new: bool = False, response_time: float = None):
|
|
9
|
+
"""Format a chat message.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
role: 'user' or 'assistant'
|
|
13
|
+
content: Message content
|
|
14
|
+
colors: Color scheme dict
|
|
15
|
+
styles: Styles dict
|
|
16
|
+
is_new: Whether this is a new message (for animation)
|
|
17
|
+
response_time: Time in seconds it took to generate the response (agent messages only)
|
|
18
|
+
"""
|
|
19
|
+
is_user = role == "user"
|
|
20
|
+
|
|
21
|
+
# Render content as markdown for assistant messages, plain text for user
|
|
22
|
+
if is_user:
|
|
23
|
+
content_element = html.Div(content, style={
|
|
24
|
+
"fontSize": "15px", "lineHeight": "1.5", "whiteSpace": "pre-wrap",
|
|
25
|
+
})
|
|
26
|
+
else:
|
|
27
|
+
content_element = dcc.Markdown(
|
|
28
|
+
content,
|
|
29
|
+
style={
|
|
30
|
+
"fontSize": "15px",
|
|
31
|
+
"lineHeight": "1.5",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Use CSS classes for theme-aware styling
|
|
36
|
+
message_class = "message-enter chat-message" if is_new else "chat-message"
|
|
37
|
+
if is_user:
|
|
38
|
+
message_class += " chat-message-user"
|
|
39
|
+
else:
|
|
40
|
+
message_class += " chat-message-agent"
|
|
41
|
+
|
|
42
|
+
# Build header with role and optional response time
|
|
43
|
+
header_children = [
|
|
44
|
+
html.Span("You" if is_user else "Agent", className="message-role-user" if is_user else "message-role-agent", style={
|
|
45
|
+
"fontSize": "12px", "fontWeight": "500",
|
|
46
|
+
"textTransform": "uppercase", "letterSpacing": "0.4px",
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Add response time for agent messages
|
|
51
|
+
if not is_user and response_time is not None:
|
|
52
|
+
if response_time >= 60:
|
|
53
|
+
minutes = int(response_time // 60)
|
|
54
|
+
seconds = int(response_time % 60)
|
|
55
|
+
time_str = f"{minutes}m {seconds}s"
|
|
56
|
+
else:
|
|
57
|
+
time_str = f"{int(response_time)}s"
|
|
58
|
+
header_children.append(
|
|
59
|
+
html.Span(time_str, className="message-time", style={
|
|
60
|
+
"fontSize": "12px", "marginLeft": "8px",
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return html.Div([
|
|
65
|
+
html.Div(header_children, style={"marginBottom": "5px"}),
|
|
66
|
+
content_element
|
|
67
|
+
], className=message_class, style={
|
|
68
|
+
"padding": "12px 15px",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def format_loading(colors: Dict):
|
|
73
|
+
"""Format loading indicator."""
|
|
74
|
+
return html.Div([
|
|
75
|
+
html.Div([
|
|
76
|
+
html.Span("Agent", className="message-role-agent", style={
|
|
77
|
+
"fontSize": "12px", "fontWeight": "500",
|
|
78
|
+
"textTransform": "uppercase",
|
|
79
|
+
}),
|
|
80
|
+
], style={"marginBottom": "5px"}),
|
|
81
|
+
html.Span("Thinking", className="loading-dots thinking-pulse thinking-text", style={
|
|
82
|
+
"fontSize": "15px", "fontWeight": "500",
|
|
83
|
+
})
|
|
84
|
+
], className="chat-message chat-message-loading", style={"padding": "12px 15px"})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def format_thinking(thinking_text: str, colors: Dict):
|
|
88
|
+
"""Format thinking as an inline subordinate message."""
|
|
89
|
+
if not thinking_text:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
return html.Details([
|
|
93
|
+
html.Summary("Thinking", className="details-summary details-summary-thinking"),
|
|
94
|
+
html.Div(thinking_text, className="details-content details-content-thinking", style={
|
|
95
|
+
"whiteSpace": "pre-wrap",
|
|
96
|
+
})
|
|
97
|
+
], className="chat-details", style={
|
|
98
|
+
"marginBottom": "4px",
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def format_todos(todos, colors: Dict):
|
|
103
|
+
"""Format todo list. Handles both list format [{"content": ..., "status": ...}] and dict format {task_name: status}."""
|
|
104
|
+
if not todos:
|
|
105
|
+
return html.Span("No tasks", style={"color": colors["text_muted"], "fontStyle": "italic", "fontSize": "14px"})
|
|
106
|
+
|
|
107
|
+
items = []
|
|
108
|
+
|
|
109
|
+
# Normalize to list of (task_name, status) tuples
|
|
110
|
+
todo_list = []
|
|
111
|
+
if isinstance(todos, list):
|
|
112
|
+
# List format: [{"content": "task", "status": "pending"}, ...]
|
|
113
|
+
for item in todos:
|
|
114
|
+
if isinstance(item, dict):
|
|
115
|
+
task_name = item.get("content", "")
|
|
116
|
+
status = item.get("status", "pending")
|
|
117
|
+
todo_list.append((task_name, status))
|
|
118
|
+
elif isinstance(todos, dict):
|
|
119
|
+
# Dict format: {task_name: status}
|
|
120
|
+
for task_name, status in todos.items():
|
|
121
|
+
todo_list.append((task_name, status))
|
|
122
|
+
|
|
123
|
+
for task_name, status in todo_list:
|
|
124
|
+
# Determine checkbox symbol and styling based on status
|
|
125
|
+
if status == "completed":
|
|
126
|
+
checkbox_symbol = "✓"
|
|
127
|
+
checkbox_color = colors["todo"]
|
|
128
|
+
text_color = colors["text_muted"]
|
|
129
|
+
text_decoration = "line-through"
|
|
130
|
+
font_weight = "bold"
|
|
131
|
+
elif status == "in_progress":
|
|
132
|
+
checkbox_symbol = "◐"
|
|
133
|
+
checkbox_color = colors["warning"]
|
|
134
|
+
text_color = colors["text_primary"]
|
|
135
|
+
text_decoration = "none"
|
|
136
|
+
font_weight = "bold"
|
|
137
|
+
else: # pending
|
|
138
|
+
checkbox_symbol = "○"
|
|
139
|
+
checkbox_color = colors["text_muted"]
|
|
140
|
+
text_color = colors["text_primary"]
|
|
141
|
+
text_decoration = "none"
|
|
142
|
+
font_weight = "normal"
|
|
143
|
+
|
|
144
|
+
checkbox = html.Span(
|
|
145
|
+
checkbox_symbol,
|
|
146
|
+
style={
|
|
147
|
+
"fontSize": "14px",
|
|
148
|
+
"color": checkbox_color,
|
|
149
|
+
"marginRight": "8px",
|
|
150
|
+
"fontWeight": font_weight,
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
items.append(html.Div([
|
|
155
|
+
checkbox,
|
|
156
|
+
html.Span(task_name, style={
|
|
157
|
+
"fontSize": "14px",
|
|
158
|
+
"color": text_color,
|
|
159
|
+
"textDecoration": text_decoration,
|
|
160
|
+
"fontWeight": font_weight if status == "in_progress" else "normal",
|
|
161
|
+
})
|
|
162
|
+
], style={"display": "flex", "alignItems": "center", "marginBottom": "4px"}))
|
|
163
|
+
|
|
164
|
+
return html.Div(items)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def format_todos_inline(todos, colors: Dict):
|
|
168
|
+
"""Format todos as an inline subordinate message."""
|
|
169
|
+
if not todos:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
todo_items = format_todos(todos, colors)
|
|
173
|
+
|
|
174
|
+
return html.Details([
|
|
175
|
+
html.Summary("Tasks", className="details-summary details-summary-todo"),
|
|
176
|
+
html.Div(todo_items, className="details-content details-content-todo")
|
|
177
|
+
], open=True, className="chat-details", style={
|
|
178
|
+
"marginBottom": "4px",
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
|
|
183
|
+
"""Format a single tool call as a submessage.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
tool_call: Dict with 'name', 'args', and optionally 'result', 'status'
|
|
187
|
+
colors: Color scheme dict
|
|
188
|
+
is_completed: Whether the tool call has completed
|
|
189
|
+
"""
|
|
190
|
+
tool_name = tool_call.get("name", "unknown")
|
|
191
|
+
tool_args = tool_call.get("args", {})
|
|
192
|
+
tool_result = tool_call.get("result")
|
|
193
|
+
tool_status = tool_call.get("status", "pending")
|
|
194
|
+
|
|
195
|
+
# Status indicator - use CSS classes for theme awareness
|
|
196
|
+
if tool_status == "success":
|
|
197
|
+
status_icon = "✓"
|
|
198
|
+
status_class = "tool-call-success"
|
|
199
|
+
icon_class = "tool-call-icon-success"
|
|
200
|
+
elif tool_status == "error":
|
|
201
|
+
status_icon = "✗"
|
|
202
|
+
status_class = "tool-call-error"
|
|
203
|
+
icon_class = "tool-call-icon-error"
|
|
204
|
+
elif tool_status == "running":
|
|
205
|
+
status_icon = "◐"
|
|
206
|
+
status_class = "tool-call-running"
|
|
207
|
+
icon_class = "tool-call-icon-running"
|
|
208
|
+
else: # pending
|
|
209
|
+
status_icon = "○"
|
|
210
|
+
status_class = "tool-call-pending"
|
|
211
|
+
icon_class = "tool-call-icon-pending"
|
|
212
|
+
|
|
213
|
+
# Format args for display (truncate if too long)
|
|
214
|
+
args_display = ""
|
|
215
|
+
if tool_args:
|
|
216
|
+
try:
|
|
217
|
+
args_str = json.dumps(tool_args, indent=2)
|
|
218
|
+
if len(args_str) > 500:
|
|
219
|
+
args_str = args_str[:500] + "..."
|
|
220
|
+
args_display = args_str
|
|
221
|
+
except:
|
|
222
|
+
args_display = str(tool_args)[:500]
|
|
223
|
+
|
|
224
|
+
# Build the tool call display using CSS classes
|
|
225
|
+
tool_header = html.Div([
|
|
226
|
+
html.Span(status_icon, className=icon_class, style={
|
|
227
|
+
"marginRight": "10px",
|
|
228
|
+
"fontWeight": "bold",
|
|
229
|
+
}),
|
|
230
|
+
html.Span("Tool: ", className="tool-call-label"),
|
|
231
|
+
html.Span(tool_name, className="tool-call-name"),
|
|
232
|
+
], style={"display": "flex", "alignItems": "center"})
|
|
233
|
+
|
|
234
|
+
# Arguments section (collapsible)
|
|
235
|
+
args_section = None
|
|
236
|
+
if args_display:
|
|
237
|
+
args_section = html.Details([
|
|
238
|
+
html.Summary("Arguments", className="tool-call-summary"),
|
|
239
|
+
html.Pre(args_display, className="tool-call-args")
|
|
240
|
+
], style={"marginTop": "5px"})
|
|
241
|
+
|
|
242
|
+
# Result section (collapsible, only if completed)
|
|
243
|
+
result_section = None
|
|
244
|
+
if tool_result is not None and is_completed:
|
|
245
|
+
result_display = str(tool_result)
|
|
246
|
+
if len(result_display) > 500:
|
|
247
|
+
result_display = result_display[:500] + "..."
|
|
248
|
+
|
|
249
|
+
result_section = html.Details([
|
|
250
|
+
html.Summary("Result", className="tool-call-summary"),
|
|
251
|
+
html.Pre(result_display, className="tool-call-result")
|
|
252
|
+
], style={"marginTop": "5px"})
|
|
253
|
+
|
|
254
|
+
children = [tool_header]
|
|
255
|
+
if args_section:
|
|
256
|
+
children.append(args_section)
|
|
257
|
+
if result_section:
|
|
258
|
+
children.append(result_section)
|
|
259
|
+
|
|
260
|
+
return html.Div(children, className=f"tool-call {status_class}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
|
|
264
|
+
"""Format multiple tool calls as an inline collapsible section.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
|
|
268
|
+
colors: Color scheme dict
|
|
269
|
+
"""
|
|
270
|
+
if not tool_calls:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
# Count statuses
|
|
274
|
+
completed = sum(1 for tc in tool_calls if tc.get("status") in ("success", "error"))
|
|
275
|
+
total = len(tool_calls)
|
|
276
|
+
running = sum(1 for tc in tool_calls if tc.get("status") == "running")
|
|
277
|
+
|
|
278
|
+
# Summary text and class
|
|
279
|
+
if running > 0:
|
|
280
|
+
summary_text = f"Tools ({completed}/{total}, {running} running)"
|
|
281
|
+
summary_class = "details-summary details-summary-warning"
|
|
282
|
+
elif completed == total:
|
|
283
|
+
summary_text = f"Tools ({total} done)"
|
|
284
|
+
summary_class = "details-summary details-summary-success"
|
|
285
|
+
else:
|
|
286
|
+
summary_text = f"Tools ({completed}/{total})"
|
|
287
|
+
summary_class = "details-summary details-summary-muted"
|
|
288
|
+
|
|
289
|
+
tool_elements = [
|
|
290
|
+
format_tool_call(tc, colors, is_completed=tc.get("status") in ("success", "error"))
|
|
291
|
+
for tc in tool_calls
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
return html.Details([
|
|
295
|
+
html.Summary(summary_text, className=summary_class),
|
|
296
|
+
html.Div(tool_elements, style={
|
|
297
|
+
"paddingLeft": "10px",
|
|
298
|
+
})
|
|
299
|
+
], open=False, className="chat-details", style={
|
|
300
|
+
"marginBottom": "5px",
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def format_interrupt(interrupt_data: Dict, colors: Dict):
|
|
305
|
+
"""Format an interrupt request for human-in-the-loop interaction.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
interrupt_data: Dict with 'action_requests' and/or 'message'
|
|
309
|
+
colors: Color scheme dict
|
|
310
|
+
"""
|
|
311
|
+
if not interrupt_data:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
message = interrupt_data.get("message", "The agent needs your input to continue.")
|
|
315
|
+
action_requests = interrupt_data.get("action_requests", [])
|
|
316
|
+
|
|
317
|
+
children = [
|
|
318
|
+
html.Div([
|
|
319
|
+
html.Span("Action Required", className="interrupt-title", style={
|
|
320
|
+
"fontSize": "14px",
|
|
321
|
+
"fontWeight": "600",
|
|
322
|
+
"textTransform": "uppercase",
|
|
323
|
+
"letterSpacing": "0.5px",
|
|
324
|
+
})
|
|
325
|
+
], style={"marginBottom": "12px"}),
|
|
326
|
+
html.P(message, className="interrupt-message", style={
|
|
327
|
+
"fontSize": "15px",
|
|
328
|
+
"marginBottom": "15px",
|
|
329
|
+
})
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
# Show action requests if any
|
|
333
|
+
if action_requests:
|
|
334
|
+
for i, action in enumerate(action_requests):
|
|
335
|
+
action_type = action.get("type", "unknown")
|
|
336
|
+
action_tool = action.get("tool", "")
|
|
337
|
+
action_args = action.get("args", {})
|
|
338
|
+
|
|
339
|
+
action_children = [
|
|
340
|
+
html.Span("Tool: ", className="interrupt-tool-label", style={
|
|
341
|
+
"fontSize": "14px",
|
|
342
|
+
}),
|
|
343
|
+
html.Span(f"{action_tool}", className="interrupt-tool-name", style={
|
|
344
|
+
"fontSize": "15px",
|
|
345
|
+
"fontWeight": "600",
|
|
346
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
347
|
+
}),
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
children.append(html.Div(action_children, style={"marginBottom": "10px"}))
|
|
351
|
+
|
|
352
|
+
# Show args for bash command specifically
|
|
353
|
+
if action_tool == "bash" and action_args:
|
|
354
|
+
command = action_args.get("command", "")
|
|
355
|
+
if command:
|
|
356
|
+
children.append(html.Div([
|
|
357
|
+
html.Span("Command: ", className="interrupt-tool-label", style={
|
|
358
|
+
"fontSize": "14px",
|
|
359
|
+
}),
|
|
360
|
+
html.Pre(command, className="interrupt-command", style={
|
|
361
|
+
"fontSize": "15px",
|
|
362
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
363
|
+
"padding": "10px 15px",
|
|
364
|
+
"borderRadius": "5px",
|
|
365
|
+
"margin": "5px 0 15px 0",
|
|
366
|
+
"whiteSpace": "pre-wrap",
|
|
367
|
+
"wordBreak": "break-all",
|
|
368
|
+
})
|
|
369
|
+
]))
|
|
370
|
+
elif action_args:
|
|
371
|
+
# Show other args in a compact format
|
|
372
|
+
args_str = json.dumps(action_args, indent=2)
|
|
373
|
+
if len(args_str) > 200:
|
|
374
|
+
args_str = args_str[:200] + "..."
|
|
375
|
+
children.append(html.Pre(args_str, className="interrupt-args", style={
|
|
376
|
+
"fontSize": "14px",
|
|
377
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
378
|
+
"padding": "10px",
|
|
379
|
+
"borderRadius": "5px",
|
|
380
|
+
"margin": "5px 0 15px 0",
|
|
381
|
+
"maxHeight": "125px",
|
|
382
|
+
"overflow": "auto",
|
|
383
|
+
}))
|
|
384
|
+
|
|
385
|
+
# Input field for response
|
|
386
|
+
children.append(html.Div([
|
|
387
|
+
dcc.Input(
|
|
388
|
+
id="interrupt-input",
|
|
389
|
+
type="text",
|
|
390
|
+
placeholder="Type your response...",
|
|
391
|
+
className="interrupt-input",
|
|
392
|
+
style={
|
|
393
|
+
"width": "100%",
|
|
394
|
+
"padding": "12px 15px",
|
|
395
|
+
"borderRadius": "5px",
|
|
396
|
+
"fontSize": "16px",
|
|
397
|
+
"marginBottom": "10px",
|
|
398
|
+
}
|
|
399
|
+
),
|
|
400
|
+
html.Div([
|
|
401
|
+
html.Button("Approve", id="interrupt-approve-btn", n_clicks=0,
|
|
402
|
+
className="interrupt-btn interrupt-btn-approve",
|
|
403
|
+
style={"marginRight": "10px"}
|
|
404
|
+
),
|
|
405
|
+
html.Button("Reject", id="interrupt-reject-btn", n_clicks=0,
|
|
406
|
+
className="interrupt-btn interrupt-btn-reject",
|
|
407
|
+
style={"marginRight": "10px"}
|
|
408
|
+
),
|
|
409
|
+
html.Button("Edit", id="interrupt-edit-btn", n_clicks=0,
|
|
410
|
+
className="interrupt-btn interrupt-btn-edit"
|
|
411
|
+
),
|
|
412
|
+
], style={"display": "flex"})
|
|
413
|
+
]))
|
|
414
|
+
|
|
415
|
+
return html.Div(children, className="interrupt-container")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def render_canvas_items(canvas_items: List[Dict], colors: Dict) -> html.Div:
|
|
419
|
+
"""Render all canvas items using CSS classes for theme awareness."""
|
|
420
|
+
if not canvas_items:
|
|
421
|
+
return html.Div([
|
|
422
|
+
html.P("Canvas empty", className="canvas-empty-text", style={
|
|
423
|
+
"textAlign": "center",
|
|
424
|
+
"fontSize": "14px"
|
|
425
|
+
}),
|
|
426
|
+
html.P("Visualizations and outputs appear here", className="canvas-empty-text", style={
|
|
427
|
+
"textAlign": "center",
|
|
428
|
+
"fontSize": "12px",
|
|
429
|
+
"marginTop": "5px"
|
|
430
|
+
})
|
|
431
|
+
], className="canvas-empty", style={
|
|
432
|
+
"display": "flex",
|
|
433
|
+
"flexDirection": "column",
|
|
434
|
+
"alignItems": "center",
|
|
435
|
+
"justifyContent": "center",
|
|
436
|
+
"height": "100%",
|
|
437
|
+
"padding": "25px"
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
rendered_items = []
|
|
441
|
+
|
|
442
|
+
for i, item in enumerate(canvas_items):
|
|
443
|
+
item_type = item.get("type", "unknown")
|
|
444
|
+
title = item.get("title")
|
|
445
|
+
|
|
446
|
+
# Add title if present
|
|
447
|
+
if title:
|
|
448
|
+
rendered_items.append(
|
|
449
|
+
html.H3(title, className="canvas-item-title", style={
|
|
450
|
+
"fontSize": "15px",
|
|
451
|
+
"fontWeight": "600",
|
|
452
|
+
"marginBottom": "8px",
|
|
453
|
+
"marginTop": "15px" if i > 0 else "0",
|
|
454
|
+
})
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Render based on type
|
|
458
|
+
if item_type == "markdown":
|
|
459
|
+
rendered_items.append(
|
|
460
|
+
html.Div([
|
|
461
|
+
dcc.Markdown(
|
|
462
|
+
item.get("data", ""),
|
|
463
|
+
className="canvas-markdown",
|
|
464
|
+
style={
|
|
465
|
+
"fontSize": "15px",
|
|
466
|
+
"lineHeight": "1.5",
|
|
467
|
+
"wordBreak": "break-word",
|
|
468
|
+
"overflowWrap": "break-word",
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
], className="canvas-item canvas-item-markdown")
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
elif item_type == "dataframe":
|
|
475
|
+
rendered_items.append(
|
|
476
|
+
html.Div([
|
|
477
|
+
dcc.Markdown(
|
|
478
|
+
item.get("html", ""),
|
|
479
|
+
dangerously_allow_html=True,
|
|
480
|
+
style={"fontSize": "14px"}
|
|
481
|
+
)
|
|
482
|
+
], className="canvas-item canvas-item-dataframe", style={
|
|
483
|
+
"overflowX": "auto",
|
|
484
|
+
})
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
elif item_type == "matplotlib" or item_type == "image":
|
|
488
|
+
img_data = item.get("data", "")
|
|
489
|
+
rendered_items.append(
|
|
490
|
+
html.Div([
|
|
491
|
+
html.Img(
|
|
492
|
+
src=f"data:image/png;base64,{img_data}",
|
|
493
|
+
style={
|
|
494
|
+
"maxWidth": "100%",
|
|
495
|
+
"width": "100%",
|
|
496
|
+
"height": "auto",
|
|
497
|
+
"borderRadius": "5px",
|
|
498
|
+
"objectFit": "contain",
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
], className="canvas-item canvas-item-image", style={
|
|
502
|
+
"textAlign": "center",
|
|
503
|
+
})
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
elif item_type == "plotly":
|
|
507
|
+
fig_data = item.get("data", {})
|
|
508
|
+
rendered_items.append(
|
|
509
|
+
html.Div([
|
|
510
|
+
dcc.Graph(
|
|
511
|
+
figure=fig_data,
|
|
512
|
+
style={"height": "400px", "width": "100%"},
|
|
513
|
+
responsive=True,
|
|
514
|
+
config={
|
|
515
|
+
"displayModeBar": True,
|
|
516
|
+
"displaylogo": False,
|
|
517
|
+
"modeBarButtonsToRemove": ["lasso2d", "select2d"],
|
|
518
|
+
"responsive": True,
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
], className="canvas-item canvas-item-plotly")
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
elif item_type == "mermaid":
|
|
525
|
+
# Mermaid diagram
|
|
526
|
+
mermaid_code = item.get("data", "")
|
|
527
|
+
rendered_items.append(
|
|
528
|
+
html.Div([
|
|
529
|
+
html.Div(
|
|
530
|
+
mermaid_code,
|
|
531
|
+
className="mermaid-diagram",
|
|
532
|
+
style={
|
|
533
|
+
"textAlign": "center",
|
|
534
|
+
"padding": "25px",
|
|
535
|
+
"width": "100%",
|
|
536
|
+
"overflow": "auto",
|
|
537
|
+
"whiteSpace": "pre",
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
], className="canvas-item canvas-item-mermaid", style={
|
|
541
|
+
"textAlign": "center",
|
|
542
|
+
"overflow": "auto",
|
|
543
|
+
})
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
else:
|
|
547
|
+
# Unknown type
|
|
548
|
+
rendered_items.append(
|
|
549
|
+
html.Div([
|
|
550
|
+
html.Code(
|
|
551
|
+
str(item),
|
|
552
|
+
className="canvas-code",
|
|
553
|
+
style={
|
|
554
|
+
"fontSize": "15px",
|
|
555
|
+
"display": "block",
|
|
556
|
+
"whiteSpace": "pre-wrap",
|
|
557
|
+
"wordBreak": "break-word",
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
], className="canvas-item canvas-item-code", style={
|
|
561
|
+
"overflow": "auto",
|
|
562
|
+
})
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
return html.Div(rendered_items, style={
|
|
566
|
+
"maxWidth": "100%",
|
|
567
|
+
"overflow": "hidden",
|
|
568
|
+
})
|
cowork_dash/config.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration file for Cowork Dash.
|
|
3
|
+
|
|
4
|
+
This file is OPTIONAL and provides sensible defaults. You typically don't need to edit it.
|
|
5
|
+
|
|
6
|
+
Configuration Priority (highest to lowest):
|
|
7
|
+
1. CLI arguments (--workspace, --port, etc.)
|
|
8
|
+
2. Environment variables (DEEPAGENT_*)
|
|
9
|
+
3. This config file defaults
|
|
10
|
+
|
|
11
|
+
For most use cases, prefer environment variables or CLI arguments:
|
|
12
|
+
|
|
13
|
+
# Using environment variables (recommended for deployment)
|
|
14
|
+
export DEEPAGENT_WORKSPACE_ROOT=/my/project
|
|
15
|
+
export DEEPAGENT_PORT=9000
|
|
16
|
+
cowork-dash run
|
|
17
|
+
|
|
18
|
+
# Using CLI arguments (recommended for development)
|
|
19
|
+
cowork-dash run --workspace /my/project --port 9000
|
|
20
|
+
|
|
21
|
+
Only edit this file if you want to set project-specific defaults that apply
|
|
22
|
+
when no environment variables or CLI arguments are provided.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_config(key: str, default=None, type_cast=None):
|
|
30
|
+
"""
|
|
31
|
+
Get configuration value with priority:
|
|
32
|
+
1. Environment variable DEEPAGENT_{KEY}
|
|
33
|
+
2. Default value
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
key: Configuration key (will be uppercased for env var)
|
|
37
|
+
default: Default value if env var not set
|
|
38
|
+
type_cast: Optional function to cast env var value
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Configuration value
|
|
42
|
+
"""
|
|
43
|
+
env_value = os.getenv(f"DEEPAGENT_{key.upper()}")
|
|
44
|
+
if env_value is not None:
|
|
45
|
+
return type_cast(env_value) if type_cast else env_value
|
|
46
|
+
return default
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Workspace root directory
|
|
50
|
+
# Environment variable: DEEPAGENT_WORKSPACE_ROOT
|
|
51
|
+
# CLI argument: --workspace
|
|
52
|
+
# Default: current directory
|
|
53
|
+
_workspace_path = get_config("workspace_root", default="./")
|
|
54
|
+
WORKSPACE_ROOT = Path(_workspace_path).resolve() if _workspace_path else Path("./").resolve()
|
|
55
|
+
|
|
56
|
+
# Agent specification (format: "module_path:variable_name")
|
|
57
|
+
# Environment variable: DEEPAGENT_SPEC (or DEEPAGENT_AGENT_SPEC for backwards compatibility)
|
|
58
|
+
# CLI argument: --agent
|
|
59
|
+
# Default: None (manual mode, no agent)
|
|
60
|
+
# Example: "mymodule:agent" or "/path/to/agent.py:my_agent"
|
|
61
|
+
_default_agent = str(Path(__file__).parent / "agent.py") + ":agent"
|
|
62
|
+
AGENT_SPEC = get_config("spec", default=None) or get_config("agent_spec", default=None) or _default_agent
|
|
63
|
+
|
|
64
|
+
# Application title
|
|
65
|
+
# Environment variable: DEEPAGENT_APP_TITLE
|
|
66
|
+
# CLI argument: --title
|
|
67
|
+
APP_TITLE = get_config("app_title", default="Cowork Dash")
|
|
68
|
+
|
|
69
|
+
# Application subtitle
|
|
70
|
+
# Environment variable: DEEPAGENT_APP_SUBTITLE
|
|
71
|
+
# CLI argument: --subtitle
|
|
72
|
+
APP_SUBTITLE = get_config("app_subtitle", default="AI-Powered Workspace")
|
|
73
|
+
|
|
74
|
+
# Server port
|
|
75
|
+
# Environment variable: DEEPAGENT_PORT
|
|
76
|
+
# CLI argument: --port
|
|
77
|
+
PORT = get_config("port", default=8050, type_cast=int)
|
|
78
|
+
|
|
79
|
+
# Server host (use "0.0.0.0" to allow external connections)
|
|
80
|
+
# Environment variable: DEEPAGENT_HOST
|
|
81
|
+
# CLI argument: --host
|
|
82
|
+
HOST = get_config("host", default="localhost")
|
|
83
|
+
|
|
84
|
+
# Debug mode (set to True for development, False for production)
|
|
85
|
+
# Environment variable: DEEPAGENT_DEBUG (accepts: true/1/yes)
|
|
86
|
+
# CLI argument: --debug
|
|
87
|
+
DEBUG = get_config(
|
|
88
|
+
"debug",
|
|
89
|
+
default=False,
|
|
90
|
+
type_cast=lambda x: str(x).lower() in ("true", "1", "yes")
|
|
91
|
+
)
|