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,226 @@
1
+ """File tree and file operations utilities."""
2
+
3
+ import base64
4
+ from pathlib import Path
5
+ from typing import List, Dict, Tuple
6
+ from dash import html
7
+
8
+
9
+ TEXT_EXTENSIONS = {
10
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css", ".json", ".md", ".txt",
11
+ ".yaml", ".yml", ".toml", ".xml", ".csv", ".sh", ".bash", ".sql", ".env",
12
+ ".gitignore", ".dockerignore", ".cfg", ".ini", ".conf", ".log"
13
+ }
14
+
15
+
16
+ def is_text_file(filename: str) -> bool:
17
+ """Check if a file can be viewed as text."""
18
+ ext = Path(filename).suffix.lower()
19
+ return ext in TEXT_EXTENSIONS or ext == ""
20
+
21
+
22
+ def build_file_tree(root: Path, workspace_root: Path, lazy_load: bool = True) -> List[Dict]:
23
+ """
24
+ Build file tree structure.
25
+
26
+ Args:
27
+ root: Directory to scan
28
+ workspace_root: Root workspace directory for relative paths
29
+ lazy_load: If True, only load immediate children (subdirs not expanded)
30
+
31
+ Returns:
32
+ List of file/folder items
33
+ """
34
+ items = []
35
+ try:
36
+ entries = sorted(root.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
37
+ for entry in entries:
38
+ if entry.name.startswith('.'):
39
+ continue
40
+ rel_path = str(entry.relative_to(workspace_root))
41
+ if entry.is_dir():
42
+ # Count immediate children to show if folder is empty
43
+ try:
44
+ has_children = any(not item.name.startswith('.') for item in entry.iterdir())
45
+ except (PermissionError, OSError):
46
+ has_children = False
47
+
48
+ items.append({
49
+ "type": "folder",
50
+ "name": entry.name,
51
+ "path": rel_path,
52
+ "has_children": has_children,
53
+ # Only recursively load children if not lazy loading
54
+ "children": [] if lazy_load else build_file_tree(entry, workspace_root, lazy_load=False)
55
+ })
56
+ else:
57
+ items.append({
58
+ "type": "file",
59
+ "name": entry.name,
60
+ "path": rel_path,
61
+ "viewable": is_text_file(entry.name)
62
+ })
63
+ except PermissionError:
64
+ pass
65
+ return items
66
+
67
+
68
+ def load_folder_contents(folder_path: str, workspace_root: Path) -> List[Dict]:
69
+ """
70
+ Load contents of a specific folder (for lazy loading).
71
+
72
+ Args:
73
+ folder_path: Relative path to the folder from workspace root
74
+ workspace_root: Root workspace directory
75
+
76
+ Returns:
77
+ List of file/folder items in the specified folder
78
+ """
79
+ full_path = workspace_root / folder_path
80
+ return build_file_tree(full_path, workspace_root, lazy_load=True)
81
+
82
+
83
+ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int = 0, parent_path: str = "") -> List:
84
+ """Render file tree with collapsible folders using CSS classes for theming."""
85
+ components = []
86
+ indent = level * 15 # Scaled up indent
87
+
88
+ for item in items:
89
+ if item["type"] == "folder":
90
+ folder_id = item["path"].replace("/", "_").replace("\\", "_")
91
+ children = item.get("children", [])
92
+
93
+ # Folder header (clickable to expand/collapse)
94
+ components.append(
95
+ html.Div([
96
+ html.Span(
97
+ "▶",
98
+ id={"type": "folder-icon", "path": folder_id},
99
+ className="folder-icon",
100
+ style={
101
+ "marginRight": "5px",
102
+ "fontSize": "10px",
103
+ "transition": "transform 0.15s",
104
+ "display": "inline-block",
105
+ }
106
+ ),
107
+ html.Span(item["name"], className="folder-name", style={
108
+ "fontWeight": "500",
109
+ "fontSize": "14px",
110
+ })
111
+ ],
112
+ id={"type": "folder-header", "path": folder_id},
113
+ **{"data-realpath": item["path"]}, # Store actual path for lazy loading
114
+ className="folder-header file-tree-item",
115
+ style={
116
+ "display": "flex",
117
+ "alignItems": "center",
118
+ "padding": "5px 10px",
119
+ "paddingLeft": f"{10 + indent}px",
120
+ "cursor": "pointer",
121
+ "userSelect": "none",
122
+ },
123
+ )
124
+ )
125
+
126
+ # Folder children (hidden by default) - always create even if empty
127
+ # Show different content based on whether children are loaded
128
+ has_children = item.get("has_children", True)
129
+
130
+ if children:
131
+ # Children are loaded, render them
132
+ child_content = render_file_tree(children, colors, styles, level + 1, item["path"])
133
+ elif not has_children:
134
+ # Folder is known to be empty
135
+ child_content = [
136
+ html.Div("(empty)", className="file-tree-empty", style={
137
+ "padding": "4px 10px",
138
+ "paddingLeft": f"{25 + (level + 1) * 15}px",
139
+ "fontSize": "12px",
140
+ "fontStyle": "italic",
141
+ })
142
+ ]
143
+ else:
144
+ # Children not yet loaded (lazy loading)
145
+ child_content = [
146
+ html.Div("Loading...",
147
+ id={"type": "folder-loading", "path": folder_id},
148
+ className="file-tree-loading",
149
+ style={
150
+ "padding": "4px 10px",
151
+ "paddingLeft": f"{25 + (level + 1) * 15}px",
152
+ "fontSize": "12px",
153
+ "fontStyle": "italic",
154
+ }
155
+ )
156
+ ]
157
+
158
+ components.append(
159
+ html.Div(
160
+ child_content,
161
+ id={"type": "folder-children", "path": folder_id},
162
+ style={"display": "none"} # Collapsed by default
163
+ )
164
+ )
165
+ else:
166
+ # File item
167
+ components.append(
168
+ html.Div(
169
+ item["name"],
170
+ id={"type": "file-item", "path": item["path"]},
171
+ className="file-item file-tree-item",
172
+ style={
173
+ "fontSize": "14px",
174
+ "padding": "5px 10px",
175
+ "paddingLeft": f"{25 + indent}px",
176
+ "cursor": "pointer",
177
+ },
178
+ **{"data-viewable": "true" if item["viewable"] else "false"}
179
+ )
180
+ )
181
+
182
+ return components
183
+
184
+
185
+ def read_file_content(workspace_root: Path, path: str) -> Tuple[str, bool, str]:
186
+ """Read file content. Returns (content, is_text, error)."""
187
+ full_path = workspace_root / path
188
+ if not full_path.exists() or not full_path.is_file():
189
+ return None, False, "File not found"
190
+
191
+ if is_text_file(path):
192
+ try:
193
+ content = full_path.read_text(encoding="utf-8")
194
+ return content, True, None
195
+ except UnicodeDecodeError:
196
+ return None, False, "Binary file - cannot display"
197
+ except Exception as e:
198
+ return None, False, str(e)
199
+ else:
200
+ return None, False, "Binary file - download to view"
201
+
202
+
203
+ def get_file_download_data(workspace_root: Path, path: str) -> Tuple[str, str, str]:
204
+ """Get file data for download. Returns (base64_content, filename, mime_type)."""
205
+ full_path = workspace_root / path
206
+ if not full_path.exists():
207
+ return None, None, None
208
+
209
+ try:
210
+ content = full_path.read_bytes()
211
+ b64 = base64.b64encode(content).decode('utf-8')
212
+
213
+ # Determine MIME type
214
+ ext = full_path.suffix.lower()
215
+ mime_types = {
216
+ ".txt": "text/plain", ".py": "text/x-python", ".js": "text/javascript",
217
+ ".json": "application/json", ".html": "text/html", ".css": "text/css",
218
+ ".md": "text/markdown", ".csv": "text/csv", ".xml": "text/xml",
219
+ ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg",
220
+ ".gif": "image/gif", ".zip": "application/zip",
221
+ }
222
+ mime = mime_types.get(ext, "application/octet-stream")
223
+
224
+ return b64, full_path.name, mime
225
+ except Exception:
226
+ return None, None, None
cowork_dash/layout.py ADDED
@@ -0,0 +1,250 @@
1
+ """Layout components for DeepAgent Dash."""
2
+
3
+ from dash import html, dcc
4
+ import dash_mantine_components as dmc
5
+ from dash_iconify import DashIconify
6
+
7
+ from .file_utils import build_file_tree, render_file_tree
8
+
9
+
10
+ def create_layout(workspace_root, app_title, app_subtitle, colors, styles, agent):
11
+ """
12
+ Create the app layout with current configuration.
13
+
14
+ Args:
15
+ workspace_root: Path to workspace directory
16
+ app_title: Application title
17
+ app_subtitle: Application subtitle
18
+ colors: Color scheme dictionary
19
+ styles: Styles dictionary
20
+ agent: Agent instance (or None)
21
+
22
+ Returns:
23
+ Dash layout component
24
+ """
25
+ return dmc.MantineProvider(
26
+ id="mantine-provider",
27
+ forceColorScheme="light",
28
+ children=[
29
+ # State stores
30
+ dcc.Store(id="chat-history", data=[{
31
+ "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!"""
40
+ }]),
41
+ dcc.Store(id="pending-message", data=None),
42
+ dcc.Store(id="expanded-folders", data=[]),
43
+ dcc.Store(id="file-to-view", data=None),
44
+ dcc.Store(id="file-click-tracker", data={}),
45
+ dcc.Store(id="theme-store", data="light", storage_type="local"),
46
+ dcc.Download(id="file-download"),
47
+
48
+ # Interval for polling agent updates (disabled by default)
49
+ dcc.Interval(id="poll-interval", interval=250, disabled=True),
50
+
51
+ # File viewer modal
52
+ dmc.Modal(
53
+ id="file-modal",
54
+ title="",
55
+ size="xl",
56
+ children=[
57
+ html.Div(id="modal-content"),
58
+ html.Div([
59
+ dmc.Button(
60
+ "Download",
61
+ id="modal-download-btn",
62
+ variant="outline",
63
+ color="blue",
64
+ style={"marginTop": "16px"}
65
+ )
66
+ ], style={"textAlign": "right"})
67
+ ],
68
+ opened=False,
69
+ ),
70
+
71
+ html.Div([
72
+ # Compact Header
73
+ html.Header([
74
+ html.Div([
75
+ html.Div([
76
+ html.H1(app_title or "DeepAgent Dash", id="app-title", style={
77
+ "fontSize": "17px", "fontWeight": "600", "margin": "0",
78
+ }),
79
+ html.Span(app_subtitle or "AI-Powered Workspace", id="app-subtitle", style={
80
+ "fontSize": "14px", "color": "var(--mantine-color-dimmed)", "marginLeft": "10px",
81
+ })
82
+ ], style={"display": "flex", "alignItems": "baseline"}),
83
+ html.Div([
84
+ dmc.ActionIcon(
85
+ DashIconify(icon="radix-icons:moon", width=18),
86
+ id="theme-toggle-btn",
87
+ variant="subtle",
88
+ color="gray",
89
+ size="md",
90
+ radius="sm",
91
+ style={"marginRight": "8px"},
92
+ ),
93
+ html.Div(style={
94
+ "width": "8px", "height": "8px",
95
+ "borderRadius": "50%",
96
+ "background": "var(--mantine-color-green-6)" if agent else "var(--mantine-color-red-6)",
97
+ "marginRight": "5px",
98
+ }, id="agent-status-indicator"),
99
+ dmc.Text("Ready" if agent else "No Agent", size="sm", c="dimmed", id="agent-status-text")
100
+ ], style={"display": "flex", "alignItems": "center"})
101
+ ], style={
102
+ "display": "flex", "justifyContent": "space-between",
103
+ "alignItems": "center", "maxWidth": "1600px",
104
+ "margin": "0 auto", "padding": "0 12px",
105
+ })
106
+ ], id="header", style={
107
+ "background": "var(--mantine-color-body)",
108
+ "borderBottom": "1px solid var(--mantine-color-default-border)",
109
+ "padding": "8px 0",
110
+ }),
111
+
112
+ # Main content
113
+ html.Main([
114
+ # Chat panel (no header)
115
+ html.Div([
116
+ # Messages
117
+ html.Div(id="chat-messages", style={
118
+ "flex": "1", "overflowY": "auto", "padding": "15px",
119
+ "display": "flex", "flexDirection": "column", "gap": "10px",
120
+ }),
121
+
122
+ # Compact Input
123
+ 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
+ dmc.TextInput(
136
+ id="chat-input",
137
+ placeholder="Type a message...",
138
+ className="chat-input",
139
+ style={"flex": "1"},
140
+ size="md",
141
+ ),
142
+ dmc.Button("Send", id="send-btn", className="send-btn", size="md"),
143
+ ], id="chat-input-area", style={
144
+ "display": "flex", "gap": "8px", "padding": "10px 15px",
145
+ "borderTop": "1px solid var(--mantine-color-default-border)",
146
+ "background": "var(--mantine-color-body)",
147
+ }),
148
+ dmc.Text(id="upload-status", size="sm", c="dimmed", style={
149
+ "padding": "0 15px 8px",
150
+ }),
151
+ ], id="chat-panel", style={
152
+ "flex": "1", "display": "flex", "flexDirection": "column",
153
+ "background": "var(--mantine-color-body)", "minWidth": "0",
154
+ }),
155
+
156
+ # Resize handle
157
+ html.Div(id="resize-handle", className="resize-handle", style={
158
+ "width": "3px",
159
+ "cursor": "col-resize",
160
+ "background": "transparent",
161
+ "flexShrink": "0",
162
+ }),
163
+
164
+ # Sidebar (Files/Canvas toggle)
165
+ html.Div([
166
+ # Compact header with toggle
167
+ html.Div([
168
+ dmc.SegmentedControl(
169
+ id="sidebar-view-toggle",
170
+ data=[
171
+ {"value": "files", "label": "Files"},
172
+ {"value": "canvas", "label": "Canvas"},
173
+ ],
174
+ value="files",
175
+ size="sm",
176
+ ),
177
+ dmc.Group([
178
+ dmc.ActionIcon(
179
+ DashIconify(icon="mdi:console", width=18),
180
+ id="open-terminal-btn",
181
+ variant="default",
182
+ size="md",
183
+ ),
184
+ dmc.ActionIcon(
185
+ DashIconify(icon="mdi:refresh", width=18),
186
+ id="refresh-btn",
187
+ variant="default",
188
+ size="md",
189
+ ),
190
+ ], id="files-actions", gap=5)
191
+ ], id="sidebar-header", style={
192
+ "display": "flex", "justifyContent": "space-between",
193
+ "alignItems": "center", "padding": "8px 12px",
194
+ "borderBottom": "1px solid var(--mantine-color-default-border)",
195
+ }),
196
+
197
+ # Files view
198
+ html.Div([
199
+ html.Div(
200
+ id="file-tree",
201
+ children=render_file_tree(build_file_tree(workspace_root, workspace_root), colors, styles),
202
+ style={
203
+ "flex": "1",
204
+ "overflowY": "auto",
205
+ "minHeight": "0",
206
+ }
207
+ ),
208
+ ], id="files-view", style={
209
+ "flex": "1",
210
+ "minHeight": "0",
211
+ "display": "flex",
212
+ "flexDirection": "column",
213
+ }),
214
+
215
+ # Canvas view (hidden by default)
216
+ html.Div([
217
+ html.Div(id="canvas-content", style={
218
+ "flex": "1",
219
+ "minHeight": "0",
220
+ "overflowY": "auto",
221
+ "padding": "15px",
222
+ "background": "var(--mantine-color-body)",
223
+ }),
224
+ # Canvas action button
225
+ dmc.Group([
226
+ dmc.Button("Clear", id="clear-canvas-btn", size="sm", color="red", variant="light"),
227
+ ], id="canvas-actions", justify="center", style={
228
+ "padding": "8px 15px",
229
+ "borderTop": "1px solid var(--mantine-color-default-border)",
230
+ "background": "var(--mantine-color-body)",
231
+ })
232
+ ], id="canvas-view", style={
233
+ "flex": "1",
234
+ "minHeight": "0",
235
+ "display": "none",
236
+ "flexDirection": "column",
237
+ "overflow": "hidden"
238
+ }),
239
+ ], id="sidebar-panel", style={
240
+ "flex": "1",
241
+ "minWidth": "0",
242
+ "minHeight": "0",
243
+ "display": "flex",
244
+ "flexDirection": "column",
245
+ "background": "var(--mantine-color-body)",
246
+ "borderLeft": "1px solid var(--mantine-color-default-border)",
247
+ }),
248
+ ], id="main-container", style={"display": "flex", "flex": "1", "overflow": "hidden"}),
249
+ ], id="app-container", style={"display": "flex", "flexDirection": "column", "height": "100vh"})
250
+ ])