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.
@@ -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
+ )