cowork-dash 0.1.9__py3-none-any.whl → 0.2.0__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/agent.py +32 -11
- cowork_dash/app.py +591 -67
- cowork_dash/assets/app.js +34 -0
- cowork_dash/assets/styles.css +788 -697
- cowork_dash/cli.py +9 -0
- cowork_dash/components.py +398 -55
- cowork_dash/config.py +12 -1
- cowork_dash/file_utils.py +43 -4
- cowork_dash/layout.py +2 -2
- cowork_dash/sandbox.py +361 -0
- cowork_dash/tools.py +640 -38
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/METADATA +1 -1
- cowork_dash-0.2.0.dist-info/RECORD +23 -0
- cowork_dash-0.1.9.dist-info/RECORD +0 -22
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/WHEEL +0 -0
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/entry_points.txt +0 -0
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/licenses/LICENSE +0 -0
cowork_dash/cli.py
CHANGED
|
@@ -148,6 +148,8 @@ cowork-dash run --help
|
|
|
148
148
|
|
|
149
149
|
def run_app_cli(args):
|
|
150
150
|
"""Run the application with CLI arguments."""
|
|
151
|
+
import platform
|
|
152
|
+
|
|
151
153
|
# Import here to avoid loading Dash when just running init
|
|
152
154
|
from .app import run_app
|
|
153
155
|
|
|
@@ -155,6 +157,13 @@ def run_app_cli(args):
|
|
|
155
157
|
# Otherwise pass None to let env var / config take precedence
|
|
156
158
|
virtual_fs = True if args.virtual_fs else None
|
|
157
159
|
|
|
160
|
+
# Warn if --virtual-fs requested on non-Linux
|
|
161
|
+
if args.virtual_fs and platform.system() != "Linux":
|
|
162
|
+
print("⚠️ Warning: --virtual-fs is only supported on Linux")
|
|
163
|
+
print(" Virtual filesystem mode requires Linux for secure bash sandboxing (bubblewrap).")
|
|
164
|
+
print(" Running in physical filesystem mode instead.\n")
|
|
165
|
+
virtual_fs = None # Let config handle it (will be False)
|
|
166
|
+
|
|
158
167
|
return run_app(
|
|
159
168
|
workspace=args.workspace,
|
|
160
169
|
agent_spec=args.agent,
|
cowork_dash/components.py
CHANGED
|
@@ -73,18 +73,12 @@ def format_message(role: str, content: str, colors: Dict, styles: Dict, is_new:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def format_loading(colors: Dict):
|
|
76
|
-
"""Format loading indicator."""
|
|
76
|
+
"""Format loading indicator with dots loader."""
|
|
77
77
|
return html.Div([
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}),
|
|
83
|
-
], style={"marginBottom": "5px"}),
|
|
84
|
-
html.Span("Thinking", className="loading-dots thinking-pulse thinking-text", style={
|
|
85
|
-
"fontSize": "15px", "fontWeight": "500",
|
|
86
|
-
})
|
|
87
|
-
], className="chat-message chat-message-loading", style={"padding": "12px 15px"})
|
|
78
|
+
dmc.Loader(type="dots", size="sm")
|
|
79
|
+
], className="chat-message chat-message-loading", style={
|
|
80
|
+
"padding": "12px 15px",
|
|
81
|
+
})
|
|
88
82
|
|
|
89
83
|
|
|
90
84
|
def format_thinking(thinking_text: str, colors: Dict):
|
|
@@ -97,7 +91,7 @@ def format_thinking(thinking_text: str, colors: Dict):
|
|
|
97
91
|
html.Div(thinking_text, className="details-content details-content-thinking", style={
|
|
98
92
|
"whiteSpace": "pre-wrap",
|
|
99
93
|
})
|
|
100
|
-
], className="chat-details", style={
|
|
94
|
+
], open=True, className="chat-details", style={
|
|
101
95
|
"marginBottom": "4px",
|
|
102
96
|
})
|
|
103
97
|
|
|
@@ -183,7 +177,7 @@ def format_todos_inline(todos, colors: Dict):
|
|
|
183
177
|
|
|
184
178
|
|
|
185
179
|
def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
|
|
186
|
-
"""Format a single tool call as a submessage.
|
|
180
|
+
"""Format a single tool call as a collapsible submessage.
|
|
187
181
|
|
|
188
182
|
Args:
|
|
189
183
|
tool_call: Dict with 'name', 'args', and optionally 'result', 'status'
|
|
@@ -195,23 +189,15 @@ def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
|
|
|
195
189
|
tool_result = tool_call.get("result")
|
|
196
190
|
tool_status = tool_call.get("status", "pending")
|
|
197
191
|
|
|
198
|
-
# Status
|
|
192
|
+
# Status class for styling
|
|
199
193
|
if tool_status == "success":
|
|
200
|
-
status_icon = "✓"
|
|
201
194
|
status_class = "tool-call-success"
|
|
202
|
-
icon_class = "tool-call-icon-success"
|
|
203
195
|
elif tool_status == "error":
|
|
204
|
-
status_icon = "✗"
|
|
205
196
|
status_class = "tool-call-error"
|
|
206
|
-
icon_class = "tool-call-icon-error"
|
|
207
197
|
elif tool_status == "running":
|
|
208
|
-
status_icon = "◐"
|
|
209
198
|
status_class = "tool-call-running"
|
|
210
|
-
icon_class = "tool-call-icon-running"
|
|
211
199
|
else: # pending
|
|
212
|
-
status_icon = "○"
|
|
213
200
|
status_class = "tool-call-pending"
|
|
214
|
-
icon_class = "tool-call-icon-pending"
|
|
215
201
|
|
|
216
202
|
# Format args for display (truncate if too long)
|
|
217
203
|
args_display = ""
|
|
@@ -224,43 +210,363 @@ def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
|
|
|
224
210
|
except:
|
|
225
211
|
args_display = str(tool_args)[:500]
|
|
226
212
|
|
|
227
|
-
# Build
|
|
228
|
-
|
|
229
|
-
html.Span(status_icon, className=icon_class, style={
|
|
230
|
-
"marginRight": "10px",
|
|
231
|
-
"fontWeight": "bold",
|
|
232
|
-
}),
|
|
233
|
-
html.Span("Tool: ", className="tool-call-label"),
|
|
234
|
-
html.Span(tool_name, className="tool-call-name"),
|
|
235
|
-
], style={"display": "flex", "alignItems": "center"})
|
|
213
|
+
# Build inner content (shown when expanded)
|
|
214
|
+
inner_children = []
|
|
236
215
|
|
|
237
|
-
# Arguments section
|
|
238
|
-
args_section = None
|
|
216
|
+
# Arguments section
|
|
239
217
|
if args_display:
|
|
240
|
-
|
|
218
|
+
inner_children.append(html.Details([
|
|
241
219
|
html.Summary("Arguments", className="tool-call-summary"),
|
|
242
220
|
html.Pre(args_display, className="tool-call-args")
|
|
243
|
-
]
|
|
221
|
+
]))
|
|
244
222
|
|
|
245
|
-
# Result section (
|
|
246
|
-
result_section = None
|
|
223
|
+
# Result section (only if completed)
|
|
247
224
|
if tool_result is not None and is_completed:
|
|
248
|
-
|
|
249
|
-
if
|
|
250
|
-
|
|
225
|
+
# Special handling for display_inline results - render them richly
|
|
226
|
+
if tool_name == "display_inline" and isinstance(tool_result, dict) and tool_result.get("type") == "display_inline":
|
|
227
|
+
inner_children.append(render_display_inline_result(tool_result, colors))
|
|
228
|
+
else:
|
|
229
|
+
result_display = str(tool_result)
|
|
230
|
+
if len(result_display) > 500:
|
|
231
|
+
result_display = result_display[:500] + "..."
|
|
232
|
+
|
|
233
|
+
inner_children.append(html.Details([
|
|
234
|
+
html.Summary("Result", className="tool-call-summary"),
|
|
235
|
+
html.Pre(result_display, className="tool-call-result")
|
|
236
|
+
]))
|
|
237
|
+
|
|
238
|
+
# Wrap entire tool call in a details element
|
|
239
|
+
return html.Details([
|
|
240
|
+
html.Summary([
|
|
241
|
+
html.Span(className="tool-call-status-dot"),
|
|
242
|
+
html.Span(tool_name, className="tool-call-name"),
|
|
243
|
+
], className="tool-call-header"),
|
|
244
|
+
html.Div(inner_children, className="tool-call-body") if inner_children else None
|
|
245
|
+
], className=f"tool-call {status_class}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def render_display_inline_result(result: Dict, colors: Dict) -> html.Div:
|
|
249
|
+
"""Render display_inline tool result as rich interactive content.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
result: The display_inline result dictionary
|
|
253
|
+
colors: Color scheme dict
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
A Dash html.Div component with the rendered content
|
|
257
|
+
"""
|
|
258
|
+
display_type = result.get("display_type", "text")
|
|
259
|
+
title = result.get("title")
|
|
260
|
+
data = result.get("data")
|
|
261
|
+
preview = result.get("preview")
|
|
262
|
+
error = result.get("error")
|
|
263
|
+
status = result.get("status", "success")
|
|
264
|
+
filename = result.get("filename")
|
|
265
|
+
downloadable = result.get("downloadable", False)
|
|
266
|
+
csv_data = result.get("csv")
|
|
267
|
+
|
|
268
|
+
# Get item ID for potential debugging
|
|
269
|
+
item_id = result.get("_item_id", "unknown")
|
|
270
|
+
|
|
271
|
+
# Build title header if present
|
|
272
|
+
header_children = []
|
|
273
|
+
if title:
|
|
274
|
+
header_children.append(
|
|
275
|
+
html.Div(title, className="display-inline-title", style={
|
|
276
|
+
"fontWeight": "600",
|
|
277
|
+
"fontSize": "15px",
|
|
278
|
+
"marginBottom": "8px",
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Error state
|
|
283
|
+
if status == "error":
|
|
284
|
+
return html.Div([
|
|
285
|
+
*header_children,
|
|
286
|
+
html.Div([
|
|
287
|
+
html.Span("Error: ", style={"fontWeight": "600"}),
|
|
288
|
+
html.Span(error or "Unknown error")
|
|
289
|
+
], className="display-inline-error", style={
|
|
290
|
+
"color": "#e53e3e",
|
|
291
|
+
"padding": "10px",
|
|
292
|
+
"borderRadius": "5px",
|
|
293
|
+
"backgroundColor": "rgba(229, 62, 62, 0.1)",
|
|
294
|
+
})
|
|
295
|
+
], className="display-inline-container")
|
|
296
|
+
|
|
297
|
+
content_element = None
|
|
298
|
+
|
|
299
|
+
# Render based on display type
|
|
300
|
+
if display_type == "image":
|
|
301
|
+
mime_type = result.get("mime_type", "image/png")
|
|
302
|
+
data_url = f"data:{mime_type};base64,{data}"
|
|
303
|
+
content_element = html.Img(
|
|
304
|
+
src=data_url,
|
|
305
|
+
className="display-inline-image",
|
|
306
|
+
style={
|
|
307
|
+
"maxWidth": "100%",
|
|
308
|
+
"maxHeight": "400px",
|
|
309
|
+
"borderRadius": "5px",
|
|
310
|
+
"objectFit": "contain",
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
elif display_type == "plotly":
|
|
315
|
+
content_element = dcc.Graph(
|
|
316
|
+
figure=data,
|
|
317
|
+
config={"displayModeBar": True, "responsive": True},
|
|
318
|
+
style={"height": "350px"},
|
|
319
|
+
className="display-inline-plotly"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
elif display_type == "dataframe":
|
|
323
|
+
# Show preview with expand option for full data
|
|
324
|
+
preview_html = preview.get("html", "") if isinstance(preview, dict) else ""
|
|
325
|
+
rows_shown = preview.get("rows_shown", 0) if isinstance(preview, dict) else 0
|
|
326
|
+
total_rows = preview.get("total_rows", 0) if isinstance(preview, dict) else 0
|
|
327
|
+
columns = preview.get("columns", []) if isinstance(preview, dict) else []
|
|
328
|
+
|
|
329
|
+
# Summary line
|
|
330
|
+
summary = f"{total_rows} rows × {len(columns)} columns"
|
|
331
|
+
if rows_shown < total_rows:
|
|
332
|
+
summary += f" (showing first {rows_shown})"
|
|
333
|
+
|
|
334
|
+
content_element = html.Div([
|
|
335
|
+
html.Div(summary, className="display-inline-df-summary", style={
|
|
336
|
+
"fontSize": "12px",
|
|
337
|
+
"marginBottom": "5px",
|
|
338
|
+
"opacity": "0.8",
|
|
339
|
+
}),
|
|
340
|
+
html.Div(
|
|
341
|
+
dcc.Markdown(preview_html, dangerously_allow_html=True),
|
|
342
|
+
className="display-inline-dataframe",
|
|
343
|
+
style={
|
|
344
|
+
"overflowX": "auto",
|
|
345
|
+
"maxHeight": "300px",
|
|
346
|
+
"overflowY": "auto",
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
])
|
|
350
|
+
|
|
351
|
+
elif display_type == "html":
|
|
352
|
+
# Show preview thumbnail with expand button
|
|
353
|
+
preview_content = preview or data
|
|
354
|
+
if len(str(preview_content)) > 500:
|
|
355
|
+
preview_content = str(preview_content)[:500] + "..."
|
|
356
|
+
|
|
357
|
+
content_element = html.Details([
|
|
358
|
+
html.Summary("HTML Content", className="tool-call-summary"),
|
|
359
|
+
html.Iframe(
|
|
360
|
+
srcDoc=data,
|
|
361
|
+
style={
|
|
362
|
+
"width": "100%",
|
|
363
|
+
"height": "300px",
|
|
364
|
+
"border": "1px solid #ddd",
|
|
365
|
+
"borderRadius": "5px",
|
|
366
|
+
"backgroundColor": "white",
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
], className="display-inline-html")
|
|
370
|
+
|
|
371
|
+
elif display_type == "pdf":
|
|
372
|
+
mime_type = result.get("mime_type", "application/pdf")
|
|
373
|
+
# Debug: Check if data exists
|
|
374
|
+
if not data:
|
|
375
|
+
content_element = html.Div(
|
|
376
|
+
"Error: PDF data is empty or missing",
|
|
377
|
+
style={"color": "red", "padding": "10px"}
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
data_url = f"data:{mime_type};base64,{data}"
|
|
381
|
+
# Use iframe instead of embed for better browser compatibility
|
|
382
|
+
content_element = html.Iframe(
|
|
383
|
+
src=data_url,
|
|
384
|
+
style={
|
|
385
|
+
"width": "100%",
|
|
386
|
+
"height": "400px",
|
|
387
|
+
"border": "none",
|
|
388
|
+
"borderRadius": "5px",
|
|
389
|
+
}
|
|
390
|
+
)
|
|
251
391
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
392
|
+
elif display_type == "json":
|
|
393
|
+
json_str = json.dumps(data, indent=2) if isinstance(data, (dict, list)) else str(data)
|
|
394
|
+
content_element = html.Details([
|
|
395
|
+
html.Summary("JSON Data", className="tool-call-summary"),
|
|
396
|
+
html.Pre(json_str, className="display-inline-json", style={
|
|
397
|
+
"maxHeight": "300px",
|
|
398
|
+
"overflowY": "auto",
|
|
399
|
+
"fontSize": "12px",
|
|
400
|
+
"padding": "10px",
|
|
401
|
+
"borderRadius": "5px",
|
|
402
|
+
})
|
|
403
|
+
])
|
|
404
|
+
|
|
405
|
+
else: # text or unknown
|
|
406
|
+
text_content = str(data) if data else ""
|
|
407
|
+
if len(text_content) > 1000:
|
|
408
|
+
content_element = html.Details([
|
|
409
|
+
html.Summary(f"Text ({len(text_content)} chars)", className="tool-call-summary"),
|
|
410
|
+
html.Pre(text_content, className="display-inline-text", style={
|
|
411
|
+
"maxHeight": "300px",
|
|
412
|
+
"overflowY": "auto",
|
|
413
|
+
"whiteSpace": "pre-wrap",
|
|
414
|
+
"fontSize": "13px",
|
|
415
|
+
})
|
|
416
|
+
])
|
|
417
|
+
else:
|
|
418
|
+
content_element = html.Pre(text_content, className="display-inline-text", style={
|
|
419
|
+
"whiteSpace": "pre-wrap",
|
|
420
|
+
"fontSize": "13px",
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
# Build footer with download option if applicable
|
|
424
|
+
footer_children = []
|
|
425
|
+
if filename:
|
|
426
|
+
footer_children.append(
|
|
427
|
+
html.Span(filename, className="display-inline-filename", style={
|
|
428
|
+
"fontSize": "11px",
|
|
429
|
+
"opacity": "0.7",
|
|
430
|
+
})
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Assemble final component with collapsible wrapper
|
|
434
|
+
# Note: Dash components evaluate to False in boolean context, so use 'is not None'
|
|
435
|
+
|
|
436
|
+
# Build the summary label for the collapsible header
|
|
437
|
+
display_type_labels = {
|
|
438
|
+
"image": "📷 Image",
|
|
439
|
+
"plotly": "📊 Chart",
|
|
440
|
+
"dataframe": "📋 Table",
|
|
441
|
+
"html": "🌐 HTML",
|
|
442
|
+
"pdf": "📄 PDF",
|
|
443
|
+
"json": "📝 JSON",
|
|
444
|
+
"text": "📝 Text",
|
|
445
|
+
"error": "⚠️ Error",
|
|
446
|
+
}
|
|
447
|
+
type_label = display_type_labels.get(display_type, "📎 Content")
|
|
448
|
+
summary_text = f"{type_label}: {title}" if title else type_label
|
|
449
|
+
if filename:
|
|
450
|
+
summary_text += f" ({filename})"
|
|
451
|
+
|
|
452
|
+
# Content to show inside the collapsible section
|
|
453
|
+
content_children = []
|
|
454
|
+
if content_element is not None:
|
|
455
|
+
content_children.append(content_element)
|
|
456
|
+
if footer_children:
|
|
457
|
+
content_children.append(
|
|
458
|
+
html.Div(footer_children, style={"marginTop": "5px"})
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Create "Add to Canvas" button with pattern-matching ID
|
|
462
|
+
# Using html.Button for reliable n_clicks behavior (DMC Tooltip can interfere with callbacks)
|
|
463
|
+
add_to_canvas_btn = html.Button(
|
|
464
|
+
DashIconify(icon="mdi:palette-outline", width=16),
|
|
465
|
+
id={"type": "add-display-to-canvas-btn", "index": item_id},
|
|
466
|
+
className="add-to-canvas-btn",
|
|
467
|
+
title="Add to Canvas",
|
|
468
|
+
style={
|
|
469
|
+
"background": "transparent",
|
|
470
|
+
"border": "none",
|
|
471
|
+
"cursor": "pointer",
|
|
472
|
+
"padding": "4px",
|
|
473
|
+
"borderRadius": "4px",
|
|
474
|
+
"display": "flex",
|
|
475
|
+
"alignItems": "center",
|
|
476
|
+
"justifyContent": "center",
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Store the result data in a hidden div for the callback to retrieve
|
|
481
|
+
result_store = dcc.Store(
|
|
482
|
+
id={"type": "display-inline-data", "index": item_id},
|
|
483
|
+
data=result
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Build summary with text and button
|
|
487
|
+
summary_content = html.Div([
|
|
488
|
+
html.Span(summary_text, className="display-inline-summary-text"),
|
|
489
|
+
add_to_canvas_btn,
|
|
490
|
+
], className="display-inline-summary-row", style={
|
|
491
|
+
"display": "flex",
|
|
492
|
+
"alignItems": "center",
|
|
493
|
+
"justifyContent": "space-between",
|
|
494
|
+
"width": "100%",
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
return html.Details([
|
|
498
|
+
html.Summary(summary_content, className="display-inline-summary"),
|
|
499
|
+
html.Div(content_children, className="display-inline-content", style={
|
|
500
|
+
"marginTop": "8px",
|
|
501
|
+
}),
|
|
502
|
+
result_store, # Hidden store for callback data
|
|
503
|
+
], className="display-inline-container", open=True, style={
|
|
504
|
+
"padding": "10px",
|
|
505
|
+
"borderRadius": "8px",
|
|
506
|
+
"marginTop": "5px",
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def extract_display_inline_results(tool_calls: List[Dict], colors: Dict) -> List:
|
|
511
|
+
"""Extract and render display_inline results prominently from tool calls.
|
|
512
|
+
|
|
513
|
+
This function finds all display_inline tool results and renders them
|
|
514
|
+
as standalone visual elements that appear alongside messages, rather
|
|
515
|
+
than buried in the tool call details.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
|
|
519
|
+
colors: Color scheme dict
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
List of rendered display_inline components (may be empty)
|
|
523
|
+
"""
|
|
524
|
+
if not tool_calls:
|
|
525
|
+
return []
|
|
526
|
+
|
|
527
|
+
results = []
|
|
528
|
+
for tc in tool_calls:
|
|
529
|
+
if tc.get("name") == "display_inline" and tc.get("status") in ("success", "error"):
|
|
530
|
+
result = tc.get("result")
|
|
531
|
+
# Check if result is a dict with display_inline structure
|
|
532
|
+
if isinstance(result, dict) and result.get("type") == "display_inline":
|
|
533
|
+
rendered = render_display_inline_result(result, colors)
|
|
534
|
+
results.append(rendered)
|
|
256
535
|
|
|
257
|
-
|
|
258
|
-
if args_section:
|
|
259
|
-
children.append(args_section)
|
|
260
|
-
if result_section:
|
|
261
|
-
children.append(result_section)
|
|
536
|
+
return results
|
|
262
537
|
|
|
263
|
-
|
|
538
|
+
|
|
539
|
+
def extract_thinking_from_tool_calls(tool_calls: List[Dict], colors: Dict) -> List:
|
|
540
|
+
"""Extract think_tool calls and render them as thinking blocks.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
|
|
544
|
+
colors: Color scheme dict
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
List of rendered thinking components (may be empty)
|
|
548
|
+
"""
|
|
549
|
+
if not tool_calls:
|
|
550
|
+
return []
|
|
551
|
+
|
|
552
|
+
results = []
|
|
553
|
+
for tc in tool_calls:
|
|
554
|
+
if tc.get("name") == "think_tool" and tc.get("status") in ("success", "error"):
|
|
555
|
+
result = tc.get("result")
|
|
556
|
+
if result:
|
|
557
|
+
# Extract thinking text from result
|
|
558
|
+
thinking_text = ""
|
|
559
|
+
if isinstance(result, str):
|
|
560
|
+
thinking_text = result
|
|
561
|
+
elif isinstance(result, dict):
|
|
562
|
+
thinking_text = result.get("reflection", str(result))
|
|
563
|
+
|
|
564
|
+
if thinking_text:
|
|
565
|
+
thinking_block = format_thinking(thinking_text, colors)
|
|
566
|
+
if thinking_block:
|
|
567
|
+
results.append(thinking_block)
|
|
568
|
+
|
|
569
|
+
return results
|
|
264
570
|
|
|
265
571
|
|
|
266
572
|
def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
|
|
@@ -269,14 +575,23 @@ def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
|
|
|
269
575
|
Args:
|
|
270
576
|
tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
|
|
271
577
|
colors: Color scheme dict
|
|
578
|
+
|
|
579
|
+
Note: think_tool calls are excluded from this rendering - they are rendered
|
|
580
|
+
separately via extract_thinking_from_tool_calls() to appear in correct order.
|
|
272
581
|
"""
|
|
273
582
|
if not tool_calls:
|
|
274
583
|
return None
|
|
275
584
|
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
585
|
+
# Filter out think_tool calls - they are rendered separately as thinking blocks
|
|
586
|
+
filtered_tool_calls = [tc for tc in tool_calls if tc.get("name") != "think_tool"]
|
|
587
|
+
|
|
588
|
+
if not filtered_tool_calls:
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
# Count statuses (on filtered list)
|
|
592
|
+
completed = sum(1 for tc in filtered_tool_calls if tc.get("status") in ("success", "error"))
|
|
593
|
+
total = len(filtered_tool_calls)
|
|
594
|
+
running = sum(1 for tc in filtered_tool_calls if tc.get("status") == "running")
|
|
280
595
|
|
|
281
596
|
# Summary text and class
|
|
282
597
|
if running > 0:
|
|
@@ -291,7 +606,7 @@ def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
|
|
|
291
606
|
|
|
292
607
|
tool_elements = [
|
|
293
608
|
format_tool_call(tc, colors, is_completed=tc.get("status") in ("success", "error"))
|
|
294
|
-
for tc in
|
|
609
|
+
for tc in filtered_tool_calls
|
|
295
610
|
]
|
|
296
611
|
|
|
297
612
|
return html.Details([
|
|
@@ -435,6 +750,8 @@ def _get_type_badge(item_type: str) -> dmc.Badge:
|
|
|
435
750
|
"image": "green",
|
|
436
751
|
"plotly": "violet",
|
|
437
752
|
"mermaid": "cyan",
|
|
753
|
+
"pdf": "red",
|
|
754
|
+
"error": "red",
|
|
438
755
|
}
|
|
439
756
|
type_labels = {
|
|
440
757
|
"markdown": "Text",
|
|
@@ -443,6 +760,8 @@ def _get_type_badge(item_type: str) -> dmc.Badge:
|
|
|
443
760
|
"image": "Image",
|
|
444
761
|
"plotly": "Plot",
|
|
445
762
|
"mermaid": "Diagram",
|
|
763
|
+
"pdf": "PDF",
|
|
764
|
+
"error": "Error",
|
|
446
765
|
}
|
|
447
766
|
color = type_colors.get(item_type, "gray")
|
|
448
767
|
label = type_labels.get(item_type, item_type.title())
|
|
@@ -611,6 +930,30 @@ def render_canvas_items(canvas_items: List[Dict], colors: Dict, collapsed_ids: O
|
|
|
611
930
|
"overflow": "auto",
|
|
612
931
|
})
|
|
613
932
|
|
|
933
|
+
elif item_type == "pdf":
|
|
934
|
+
pdf_data = item.get("data", "")
|
|
935
|
+
mime_type = item.get("mime_type", "application/pdf")
|
|
936
|
+
if not pdf_data:
|
|
937
|
+
content = html.Div([
|
|
938
|
+
html.Div("Error: PDF data is empty or missing", style={"color": "red"})
|
|
939
|
+
], className="canvas-item-content canvas-item-pdf", style={"padding": "10px"})
|
|
940
|
+
else:
|
|
941
|
+
data_url = f"data:{mime_type};base64,{pdf_data}"
|
|
942
|
+
# Use iframe instead of embed for better browser compatibility
|
|
943
|
+
content = html.Div([
|
|
944
|
+
html.Iframe(
|
|
945
|
+
src=data_url,
|
|
946
|
+
style={
|
|
947
|
+
"width": "100%",
|
|
948
|
+
"height": "500px",
|
|
949
|
+
"border": "none",
|
|
950
|
+
"borderRadius": "5px",
|
|
951
|
+
}
|
|
952
|
+
)
|
|
953
|
+
], className="canvas-item-content canvas-item-pdf", style={
|
|
954
|
+
"padding": "10px",
|
|
955
|
+
})
|
|
956
|
+
|
|
614
957
|
else:
|
|
615
958
|
# Unknown type
|
|
616
959
|
content = html.Div([
|
cowork_dash/config.py
CHANGED
|
@@ -23,9 +23,15 @@ when no environment variables or CLI arguments are provided.
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
|
+
import platform
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def is_linux() -> bool:
|
|
31
|
+
"""Check if running on Linux."""
|
|
32
|
+
return platform.system() == "Linux"
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
def get_config(key: str, default=None, type_cast=None):
|
|
30
36
|
"""
|
|
31
37
|
Get configuration value with priority:
|
|
@@ -111,18 +117,23 @@ WELCOME_MESSAGE = get_config("welcome_message", default=_default_welcome)
|
|
|
111
117
|
# Virtual filesystem mode (for multi-user deployments)
|
|
112
118
|
# Environment variable: DEEPAGENT_VIRTUAL_FS
|
|
113
119
|
# Accepts: true/1/yes to enable
|
|
120
|
+
# IMPORTANT: Only available on Linux due to sandboxing requirements
|
|
114
121
|
# When enabled:
|
|
115
122
|
# - Each browser session gets isolated in-memory file storage
|
|
116
123
|
# - Files, canvas, and uploads are not shared between sessions
|
|
117
124
|
# - All data is ephemeral (cleared when session ends)
|
|
125
|
+
# - Bash commands run in bubblewrap sandbox for security
|
|
118
126
|
# When disabled (default):
|
|
119
127
|
# - All sessions share the same workspace directory on disk
|
|
120
128
|
# - Files persist on disk
|
|
121
|
-
|
|
129
|
+
_virtual_fs_requested = get_config(
|
|
122
130
|
"virtual_fs",
|
|
123
131
|
default=False,
|
|
124
132
|
type_cast=lambda x: str(x).lower() in ("true", "1", "yes")
|
|
125
133
|
)
|
|
134
|
+
# Virtual FS is only supported on Linux (requires bubblewrap for bash sandboxing)
|
|
135
|
+
VIRTUAL_FS = _virtual_fs_requested and is_linux()
|
|
136
|
+
VIRTUAL_FS_UNAVAILABLE_REASON = None if is_linux() else "Virtual filesystem mode requires Linux (uses bubblewrap for bash sandboxing)"
|
|
126
137
|
|
|
127
138
|
# Session timeout in seconds (only used when VIRTUAL_FS is True)
|
|
128
139
|
# Environment variable: DEEPAGENT_SESSION_TIMEOUT
|
cowork_dash/file_utils.py
CHANGED
|
@@ -136,8 +136,18 @@ def load_folder_contents(
|
|
|
136
136
|
return build_file_tree(full_path, workspace_root, lazy_load=True)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int = 0, parent_path: str = "", expanded_folders: List[str] = None) -> List:
|
|
140
|
-
"""Render file tree with collapsible folders using CSS classes for theming.
|
|
139
|
+
def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int = 0, parent_path: str = "", expanded_folders: List[str] = None, workspace_root: AnyRoot = None) -> List:
|
|
140
|
+
"""Render file tree with collapsible folders using CSS classes for theming.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
items: List of file/folder items from build_file_tree
|
|
144
|
+
colors: Theme colors dict
|
|
145
|
+
styles: Style dict
|
|
146
|
+
level: Current nesting level
|
|
147
|
+
parent_path: Path of parent folder
|
|
148
|
+
expanded_folders: List of folder IDs that should be expanded
|
|
149
|
+
workspace_root: Workspace root for loading expanded folder contents
|
|
150
|
+
"""
|
|
141
151
|
components = []
|
|
142
152
|
indent = level * 15 # Scaled up indent
|
|
143
153
|
expanded_folders = expanded_folders or []
|
|
@@ -199,7 +209,7 @@ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int =
|
|
|
199
209
|
|
|
200
210
|
if children:
|
|
201
211
|
# Children are loaded, render them
|
|
202
|
-
child_content = render_file_tree(children, colors, styles, level + 1, item["path"], expanded_folders)
|
|
212
|
+
child_content = render_file_tree(children, colors, styles, level + 1, item["path"], expanded_folders, workspace_root)
|
|
203
213
|
elif not has_children:
|
|
204
214
|
# Folder is known to be empty
|
|
205
215
|
child_content = [
|
|
@@ -210,8 +220,37 @@ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int =
|
|
|
210
220
|
"fontStyle": "italic",
|
|
211
221
|
})
|
|
212
222
|
]
|
|
223
|
+
elif is_expanded and workspace_root is not None:
|
|
224
|
+
# Folder is expanded but children not loaded - load them now
|
|
225
|
+
# This happens when rebuilding the tree with preserved expanded_folders state
|
|
226
|
+
try:
|
|
227
|
+
folder_items = load_folder_contents(item["path"], workspace_root)
|
|
228
|
+
child_content = render_file_tree(folder_items, colors, styles, level + 1, item["path"], expanded_folders, workspace_root)
|
|
229
|
+
if not child_content:
|
|
230
|
+
child_content = [
|
|
231
|
+
html.Div("(empty)", className="file-tree-empty", style={
|
|
232
|
+
"padding": "4px 10px",
|
|
233
|
+
"paddingLeft": f"{25 + (level + 1) * 15}px",
|
|
234
|
+
"fontSize": "12px",
|
|
235
|
+
"fontStyle": "italic",
|
|
236
|
+
})
|
|
237
|
+
]
|
|
238
|
+
except Exception:
|
|
239
|
+
# Fall back to loading placeholder if loading fails
|
|
240
|
+
child_content = [
|
|
241
|
+
html.Div("Loading...",
|
|
242
|
+
id={"type": "folder-loading", "path": folder_id},
|
|
243
|
+
className="file-tree-loading",
|
|
244
|
+
style={
|
|
245
|
+
"padding": "4px 10px",
|
|
246
|
+
"paddingLeft": f"{25 + (level + 1) * 15}px",
|
|
247
|
+
"fontSize": "12px",
|
|
248
|
+
"fontStyle": "italic",
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
]
|
|
213
252
|
else:
|
|
214
|
-
# Children not yet loaded (lazy loading)
|
|
253
|
+
# Children not yet loaded (lazy loading) and folder is collapsed
|
|
215
254
|
child_content = [
|
|
216
255
|
html.Div("Loading...",
|
|
217
256
|
id={"type": "folder-loading", "path": folder_id},
|