cowork-dash 0.1.4__py3-none-any.whl → 0.1.5__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/components.py CHANGED
@@ -1,8 +1,11 @@
1
1
  """UI components for rendering messages, canvas items, and other UI elements."""
2
2
 
3
3
  import json
4
- from typing import Dict, List
4
+ from datetime import datetime
5
+ from typing import Dict, List, Optional
5
6
  from dash import html, dcc
7
+ import dash_mantine_components as dmc
8
+ from dash_iconify import DashIconify
6
9
 
7
10
 
8
11
  def format_message(role: str, content: str, colors: Dict, styles: Dict, is_new: bool = False, response_time: float = None):
@@ -415,8 +418,49 @@ def format_interrupt(interrupt_data: Dict, colors: Dict):
415
418
  return html.Div(children, className="interrupt-container")
416
419
 
417
420
 
418
- def render_canvas_items(canvas_items: List[Dict], colors: Dict) -> html.Div:
419
- """Render all canvas items using CSS classes for theme awareness."""
421
+ def _format_timestamp(iso_timestamp: str) -> str:
422
+ """Format ISO timestamp to human-readable format."""
423
+ try:
424
+ dt = datetime.fromisoformat(iso_timestamp)
425
+ return dt.strftime("%b %d, %H:%M")
426
+ except (ValueError, TypeError):
427
+ return ""
428
+
429
+
430
+ def _get_type_badge(item_type: str) -> dmc.Badge:
431
+ """Get a badge component for the item type."""
432
+ type_colors = {
433
+ "markdown": "gray",
434
+ "dataframe": "blue",
435
+ "matplotlib": "green",
436
+ "image": "green",
437
+ "plotly": "violet",
438
+ "mermaid": "cyan",
439
+ }
440
+ type_labels = {
441
+ "markdown": "Text",
442
+ "dataframe": "Table",
443
+ "matplotlib": "Chart",
444
+ "image": "Image",
445
+ "plotly": "Plot",
446
+ "mermaid": "Diagram",
447
+ }
448
+ color = type_colors.get(item_type, "gray")
449
+ label = type_labels.get(item_type, item_type.title())
450
+ return dmc.Badge(label, color=color, size="xs", variant="light")
451
+
452
+
453
+ def render_canvas_items(canvas_items: List[Dict], colors: Dict, collapsed_ids: Optional[List[str]] = None) -> html.Div:
454
+ """Render all canvas items using CSS classes for theme awareness.
455
+
456
+ Args:
457
+ canvas_items: List of canvas item dictionaries
458
+ colors: Color scheme dictionary
459
+ collapsed_ids: List of item IDs that should be rendered collapsed
460
+ """
461
+ if collapsed_ids is None:
462
+ collapsed_ids = []
463
+
420
464
  if not canvas_items:
421
465
  return html.Div([
422
466
  html.P("Canvas empty", className="canvas-empty-text", style={
@@ -441,126 +485,172 @@ def render_canvas_items(canvas_items: List[Dict], colors: Dict) -> html.Div:
441
485
 
442
486
  for i, item in enumerate(canvas_items):
443
487
  item_type = item.get("type", "unknown")
488
+ item_id = item.get("id", f"canvas_item_{i}")
489
+ is_collapsed = item_id in collapsed_ids
444
490
  title = item.get("title")
445
-
446
- # Add title if present
491
+ created_at = item.get("created_at", "")
492
+
493
+ # Build item header left side with collapse toggle, title, badge, and time
494
+ header_left = [
495
+ # Collapse/expand toggle - icon depends on collapsed state
496
+ dmc.ActionIcon(
497
+ DashIconify(icon="mdi:chevron-right" if is_collapsed else "mdi:chevron-down", width=16),
498
+ id={"type": "canvas-collapse-btn", "index": item_id},
499
+ variant="subtle",
500
+ color="gray",
501
+ size="sm",
502
+ className="canvas-collapse-btn",
503
+ ),
504
+ ]
447
505
  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
- })
506
+ header_left.append(
507
+ dmc.Text(title, fw=600, size="sm", className="canvas-item-title-text")
455
508
  )
509
+ header_left.append(_get_type_badge(item_type))
510
+ if created_at:
511
+ formatted_time = _format_timestamp(created_at)
512
+ if formatted_time:
513
+ header_left.append(
514
+ dmc.Text(formatted_time, size="xs", c="dimmed", className="canvas-item-time")
515
+ )
516
+
517
+ # Header right side with delete button (shows confirmation on first click)
518
+ header_right = dmc.Group([
519
+ # Delete button - first click shows confirm, second click deletes
520
+ dmc.ActionIcon(
521
+ DashIconify(icon="mdi:close", width=14),
522
+ id={"type": "canvas-delete-btn", "index": item_id},
523
+ variant="subtle",
524
+ color="gray",
525
+ size="sm",
526
+ className="canvas-delete-btn",
527
+ ),
528
+ ], gap="xs")
456
529
 
457
- # Render based on type
530
+ item_header = html.Div([
531
+ dmc.Group(header_left, gap="xs"),
532
+ header_right,
533
+ ], className="canvas-item-header")
534
+
535
+ # Render content based on type
458
536
  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
- )
537
+ content = html.Div([
538
+ dcc.Markdown(
539
+ item.get("data", ""),
540
+ className="canvas-markdown",
541
+ style={
542
+ "fontSize": "15px",
543
+ "lineHeight": "1.5",
544
+ "wordBreak": "break-word",
545
+ "overflowWrap": "break-word",
546
+ }
547
+ )
548
+ ], className="canvas-item-content canvas-item-markdown", style={"padding": "10px"})
473
549
 
474
550
  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
- )
551
+ content = html.Div([
552
+ dcc.Markdown(
553
+ item.get("html", ""),
554
+ dangerously_allow_html=True,
555
+ style={"fontSize": "14px"}
556
+ )
557
+ ], className="canvas-item-content canvas-item-dataframe", style={
558
+ "overflowX": "auto",
559
+ "padding": "10px",
560
+ })
486
561
 
487
562
  elif item_type == "matplotlib" or item_type == "image":
488
563
  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
- )
564
+ content = html.Div([
565
+ html.Img(
566
+ src=f"data:image/png;base64,{img_data}",
567
+ style={
568
+ "maxWidth": "100%",
569
+ "width": "100%",
570
+ "height": "auto",
571
+ "borderRadius": "5px",
572
+ "objectFit": "contain",
573
+ }
574
+ )
575
+ ], className="canvas-item-content canvas-item-image", style={
576
+ "textAlign": "center",
577
+ "padding": "10px",
578
+ })
505
579
 
506
580
  elif item_type == "plotly":
507
581
  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
- )
582
+ content = html.Div([
583
+ dcc.Graph(
584
+ figure=fig_data,
585
+ style={"height": "400px", "width": "100%"},
586
+ responsive=True,
587
+ config={
588
+ "displayModeBar": True,
589
+ "displaylogo": False,
590
+ "modeBarButtonsToRemove": ["lasso2d", "select2d"],
591
+ "responsive": True,
592
+ }
593
+ )
594
+ ], className="canvas-item-content canvas-item-plotly")
523
595
 
524
596
  elif item_type == "mermaid":
525
- # Mermaid diagram
526
597
  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
- )
598
+ content = html.Div([
599
+ html.Div(
600
+ mermaid_code,
601
+ className="mermaid-diagram",
602
+ style={
603
+ "textAlign": "center",
604
+ "padding": "25px",
605
+ "width": "100%",
606
+ "overflow": "auto",
607
+ "whiteSpace": "pre",
608
+ }
609
+ )
610
+ ], className="canvas-item-content canvas-item-mermaid", style={
611
+ "textAlign": "center",
612
+ "overflow": "auto",
613
+ })
545
614
 
546
615
  else:
547
616
  # 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
- )
617
+ content = html.Div([
618
+ html.Code(
619
+ str(item),
620
+ className="canvas-code",
621
+ style={
622
+ "fontSize": "15px",
623
+ "display": "block",
624
+ "whiteSpace": "pre-wrap",
625
+ "wordBreak": "break-word",
626
+ }
627
+ )
628
+ ], className="canvas-item-content canvas-item-code", style={
629
+ "overflow": "auto",
630
+ "padding": "10px",
631
+ })
632
+
633
+ # Wrap content in collapsible container - respect collapsed state
634
+ content_wrapper = html.Div(
635
+ content,
636
+ id={"type": "canvas-item-content", "index": item_id},
637
+ className="canvas-item-content-wrapper",
638
+ style={"display": "none" if is_collapsed else "block"}
639
+ )
640
+
641
+ # Wrap header and content in item container
642
+ rendered_items.append(
643
+ html.Div([
644
+ item_header,
645
+ content_wrapper,
646
+ ], id={"type": "canvas-item", "index": item_id}, className="canvas-item-container", style={
647
+ "border": "1px solid var(--mantine-color-default-border)",
648
+ "borderRadius": "6px",
649
+ "marginBottom": "12px",
650
+ "overflow": "hidden",
651
+ "background": "var(--mantine-color-body)",
652
+ })
653
+ )
564
654
 
565
655
  return html.Div(rendered_items, style={
566
656
  "maxWidth": "100%",
cowork_dash/config.py CHANGED
@@ -94,3 +94,16 @@ DEBUG = get_config(
94
94
  default=False,
95
95
  type_cast=lambda x: str(x).lower() in ("true", "1", "yes")
96
96
  )
97
+
98
+ # Welcome message shown when the app starts
99
+ # Environment variable: DEEPAGENT_WELCOME_MESSAGE
100
+ # Supports markdown formatting
101
+ _default_welcome = """This is your AI-powered workspace. I can help you write code, analyze files, create visualizations, and more.
102
+
103
+ **Getting Started:**
104
+ - Type a message below to chat with me
105
+ - Browse files on the right (click to view, ↓ to download)
106
+ - Switch to **Canvas** tab to see charts and diagrams I create
107
+
108
+ Let's get started!"""
109
+ WELCOME_MESSAGE = get_config("welcome_message", default=_default_welcome)
cowork_dash/file_utils.py CHANGED
@@ -90,24 +90,35 @@ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int =
90
90
  folder_id = item["path"].replace("/", "_").replace("\\", "_")
91
91
  children = item.get("children", [])
92
92
 
93
- # Folder header (clickable to expand/collapse)
93
+ # Folder header with expand icon and selectable name
94
94
  components.append(
95
95
  html.Div([
96
+ # Expand/collapse icon (left side)
96
97
  html.Span(
97
98
  "▶",
98
99
  id={"type": "folder-icon", "path": folder_id},
99
- className="folder-icon",
100
+ className="folder-icon folder-expand-toggle",
100
101
  style={
101
102
  "marginRight": "5px",
102
103
  "fontSize": "10px",
103
104
  "transition": "transform 0.15s",
104
105
  "display": "inline-block",
106
+ "padding": "2px",
105
107
  }
106
108
  ),
107
- html.Span(item["name"], className="folder-name", style={
108
- "fontWeight": "500",
109
- "fontSize": "14px",
110
- })
109
+ # Folder name (clickable for selection)
110
+ html.Span(item["name"],
111
+ id={"type": "folder-select", "path": folder_id},
112
+ className="folder-name folder-select-target",
113
+ **{"data-folderpath": item["path"]},
114
+ style={
115
+ "fontWeight": "500",
116
+ "fontSize": "14px",
117
+ "flex": "1",
118
+ "padding": "2px 4px",
119
+ "borderRadius": "3px",
120
+ }
121
+ )
111
122
  ],
112
123
  id={"type": "folder-header", "path": folder_id},
113
124
  **{"data-realpath": item["path"]}, # Store actual path for lazy loading
cowork_dash/layout.py CHANGED
@@ -5,9 +5,10 @@ import dash_mantine_components as dmc
5
5
  from dash_iconify import DashIconify
6
6
 
7
7
  from .file_utils import build_file_tree, render_file_tree
8
+ from .config import WELCOME_MESSAGE as DEFAULT_WELCOME_MESSAGE
8
9
 
9
10
 
10
- def create_layout(workspace_root, app_title, app_subtitle, colors, styles, agent):
11
+ def create_layout(workspace_root, app_title, app_subtitle, colors, styles, agent, welcome_message=None):
11
12
  """
12
13
  Create the app layout with current configuration.
13
14
 
@@ -18,10 +19,14 @@ def create_layout(workspace_root, app_title, app_subtitle, colors, styles, agent
18
19
  colors: Color scheme dictionary
19
20
  styles: Styles dictionary
20
21
  agent: Agent instance (or None)
22
+ welcome_message: Optional welcome message (uses default if not provided)
21
23
 
22
24
  Returns:
23
25
  Dash layout component
24
26
  """
27
+ # Use provided welcome message or fall back to default
28
+ message = welcome_message if welcome_message is not None else DEFAULT_WELCOME_MESSAGE
29
+
25
30
  return dmc.MantineProvider(
26
31
  id="mantine-provider",
27
32
  forceColorScheme="light",
@@ -29,20 +34,15 @@ def create_layout(workspace_root, app_title, app_subtitle, colors, styles, agent
29
34
  # State stores
30
35
  dcc.Store(id="chat-history", data=[{
31
36
  "role": "assistant",
32
- "content": f"""This is your AI-powered workspace. I can help you write code, analyze files, create visualizations, and more.
33
-
34
- **Getting Started:**
35
- - Type a message below to chat with me
36
- - Browse files on the right (click to view, ↓ to download)
37
- - Switch to **Canvas** tab to see charts and diagrams I create
38
-
39
- Let's get started!"""
37
+ "content": message
40
38
  }]),
41
39
  dcc.Store(id="pending-message", data=None),
42
40
  dcc.Store(id="expanded-folders", data=[]),
43
41
  dcc.Store(id="file-to-view", data=None),
44
42
  dcc.Store(id="file-click-tracker", data={}),
45
43
  dcc.Store(id="theme-store", data="light", storage_type="local"),
44
+ dcc.Store(id="current-workspace-path", data=""), # Relative path from original workspace root
45
+ dcc.Store(id="collapsed-canvas-items", data=[]), # Track which canvas items are collapsed
46
46
  dcc.Download(id="file-download"),
47
47
 
48
48
  # Interval for polling agent updates (disabled by default)
@@ -68,6 +68,62 @@ Let's get started!"""
68
68
  opened=False,
69
69
  ),
70
70
 
71
+ # Create folder modal
72
+ dmc.Modal(
73
+ id="create-folder-modal",
74
+ title="Create New Folder",
75
+ size="sm",
76
+ children=[
77
+ dmc.TextInput(
78
+ id="new-folder-name",
79
+ label="Folder name",
80
+ placeholder="Enter folder name",
81
+ style={"marginBottom": "16px"},
82
+ ),
83
+ dmc.Text(id="create-folder-error", c="red", size="sm", style={"marginBottom": "8px"}),
84
+ dmc.Group([
85
+ dmc.Button("Cancel", id="cancel-folder-btn", variant="outline", color="gray"),
86
+ dmc.Button("Create", id="confirm-folder-btn", color="blue"),
87
+ ], justify="flex-end"),
88
+ ],
89
+ opened=False,
90
+ ),
91
+
92
+ # Delete canvas item confirmation modal
93
+ dmc.Modal(
94
+ id="delete-canvas-item-modal",
95
+ title="Delete Canvas Item",
96
+ size="sm",
97
+ children=[
98
+ dmc.Text("Are you sure you want to delete this canvas item? This action cannot be undone.",
99
+ size="sm", style={"marginBottom": "16px"}),
100
+ dmc.Group([
101
+ dmc.Button("Cancel", id="cancel-delete-canvas-btn", variant="outline", color="gray"),
102
+ dmc.Button("Delete", id="confirm-delete-canvas-btn", color="red"),
103
+ ], justify="flex-end"),
104
+ ],
105
+ opened=False,
106
+ ),
107
+
108
+ # Store for canvas item ID pending deletion
109
+ dcc.Store(id="delete-canvas-item-id", data=None),
110
+
111
+ # Clear canvas confirmation modal
112
+ dmc.Modal(
113
+ id="clear-canvas-modal",
114
+ title="Clear Canvas",
115
+ size="sm",
116
+ children=[
117
+ dmc.Text("Are you sure you want to clear the entire canvas? The current canvas will be archived with a timestamp.",
118
+ size="sm", style={"marginBottom": "16px"}),
119
+ dmc.Group([
120
+ dmc.Button("Cancel", id="cancel-clear-canvas-btn", variant="outline", color="gray"),
121
+ dmc.Button("Clear", id="confirm-clear-canvas-btn", color="red"),
122
+ ], justify="flex-end"),
123
+ ],
124
+ opened=False,
125
+ ),
126
+
71
127
  html.Div([
72
128
  # Compact Header
73
129
  html.Header([
@@ -121,17 +177,6 @@ Let's get started!"""
121
177
 
122
178
  # Compact Input
123
179
  html.Div([
124
- dcc.Upload(
125
- id="file-upload",
126
- children=dmc.ActionIcon(
127
- DashIconify(icon="radix-icons:plus", width=18),
128
- id="upload-plus",
129
- variant="default",
130
- size="md",
131
- ),
132
- style={"cursor": "pointer"},
133
- multiple=True
134
- ),
135
180
  dmc.TextInput(
136
181
  id="chat-input",
137
182
  placeholder="Type a message...",
@@ -140,14 +185,20 @@ Let's get started!"""
140
185
  size="md",
141
186
  ),
142
187
  dmc.Button("Send", id="send-btn", className="send-btn", size="md"),
188
+ dmc.ActionIcon(
189
+ DashIconify(icon="mdi:stop", width=20),
190
+ id="stop-btn",
191
+ variant="filled",
192
+ color="red",
193
+ size="lg",
194
+ radius="sm",
195
+ style={"display": "none"}, # Hidden by default, shown when agent is running
196
+ ),
143
197
  ], id="chat-input-area", style={
144
198
  "display": "flex", "gap": "8px", "padding": "10px 15px",
145
199
  "borderTop": "1px solid var(--mantine-color-default-border)",
146
200
  "background": "var(--mantine-color-body)",
147
201
  }),
148
- dmc.Text(id="upload-status", size="sm", c="dimmed", style={
149
- "padding": "0 15px 8px",
150
- }),
151
202
  ], id="chat-panel", style={
152
203
  "flex": "1", "display": "flex", "flexDirection": "column",
153
204
  "background": "var(--mantine-color-body)", "minWidth": "0",
@@ -175,6 +226,22 @@ Let's get started!"""
175
226
  size="sm",
176
227
  ),
177
228
  dmc.Group([
229
+ dmc.ActionIcon(
230
+ DashIconify(icon="mdi:folder-plus-outline", width=18),
231
+ id="create-folder-btn",
232
+ variant="default",
233
+ size="md",
234
+ ),
235
+ dcc.Upload(
236
+ id="file-upload-sidebar",
237
+ children=dmc.ActionIcon(
238
+ DashIconify(icon="mdi:file-upload-outline", width=18),
239
+ id="upload-btn",
240
+ variant="default",
241
+ size="md",
242
+ ),
243
+ multiple=True,
244
+ ),
178
245
  dmc.ActionIcon(
179
246
  DashIconify(icon="mdi:console", width=18),
180
247
  id="open-terminal-btn",
@@ -196,6 +263,31 @@ Let's get started!"""
196
263
 
197
264
  # Files view
198
265
  html.Div([
266
+ # Workspace path breadcrumb navigation
267
+ html.Div([
268
+ html.Div(id="workspace-breadcrumb", children=[
269
+ html.Span([
270
+ DashIconify(icon="mdi:home", width=14, style={"marginRight": "4px"}),
271
+ "root"
272
+ ], id="breadcrumb-root", className="breadcrumb-item breadcrumb-clickable", style={
273
+ "display": "inline-flex",
274
+ "alignItems": "center",
275
+ "cursor": "pointer",
276
+ "padding": "2px 6px",
277
+ "borderRadius": "3px",
278
+ }),
279
+ ], style={
280
+ "display": "flex",
281
+ "alignItems": "center",
282
+ "flexWrap": "wrap",
283
+ "gap": "2px",
284
+ "fontSize": "13px",
285
+ }),
286
+ ], className="breadcrumb-bar", style={
287
+ "padding": "6px 10px",
288
+ "borderBottom": "1px solid var(--mantine-color-default-border)",
289
+ "background": "var(--mantine-color-gray-0)",
290
+ }),
199
291
  html.Div(
200
292
  id="file-tree",
201
293
  children=render_file_tree(build_file_tree(workspace_root, workspace_root), colors, styles),