cowork-dash 0.1.8__py3-none-any.whl → 0.1.9__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/app.py +323 -40
- cowork_dash/canvas.py +8 -0
- cowork_dash/file_utils.py +24 -7
- cowork_dash/tools.py +94 -41
- {cowork_dash-0.1.8.dist-info → cowork_dash-0.1.9.dist-info}/METADATA +1 -1
- {cowork_dash-0.1.8.dist-info → cowork_dash-0.1.9.dist-info}/RECORD +9 -9
- {cowork_dash-0.1.8.dist-info → cowork_dash-0.1.9.dist-info}/WHEEL +0 -0
- {cowork_dash-0.1.8.dist-info → cowork_dash-0.1.9.dist-info}/entry_points.txt +0 -0
- {cowork_dash-0.1.8.dist-info → cowork_dash-0.1.9.dist-info}/licenses/LICENSE +0 -0
cowork_dash/app.py
CHANGED
|
@@ -17,7 +17,15 @@ from typing import Optional, Dict, Any, List
|
|
|
17
17
|
from dotenv import load_dotenv
|
|
18
18
|
load_dotenv()
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Early pandas import to prevent circular import issues with Plotly's JSON serializer.
|
|
21
|
+
# Plotly lazily imports pandas and checks `obj is pd.NaT` which fails if pandas
|
|
22
|
+
# is partially initialized due to concurrent imports.
|
|
23
|
+
try:
|
|
24
|
+
import pandas
|
|
25
|
+
except (ImportError, AttributeError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
from dash import Dash, html, dcc, Input, Output, State, callback_context, no_update, ALL
|
|
21
29
|
from dash.exceptions import PreventUpdate
|
|
22
30
|
import dash_mantine_components as dmc
|
|
23
31
|
from dash_iconify import DashIconify
|
|
@@ -1493,7 +1501,8 @@ def handle_interrupt_response(approve_clicks, reject_clicks, edit_clicks, input_
|
|
|
1493
1501
|
@app.callback(
|
|
1494
1502
|
[Output({"type": "folder-children", "path": ALL}, "style"),
|
|
1495
1503
|
Output({"type": "folder-icon", "path": ALL}, "style"),
|
|
1496
|
-
Output({"type": "folder-children", "path": ALL}, "children")
|
|
1504
|
+
Output({"type": "folder-children", "path": ALL}, "children"),
|
|
1505
|
+
Output("expanded-folders", "data")],
|
|
1497
1506
|
Input({"type": "folder-icon", "path": ALL}, "n_clicks"),
|
|
1498
1507
|
[State({"type": "folder-header", "path": ALL}, "id"),
|
|
1499
1508
|
State({"type": "folder-header", "path": ALL}, "data-realpath"),
|
|
@@ -1503,16 +1512,18 @@ def handle_interrupt_response(approve_clicks, reject_clicks, edit_clicks, input_
|
|
|
1503
1512
|
State({"type": "folder-icon", "path": ALL}, "style"),
|
|
1504
1513
|
State({"type": "folder-children", "path": ALL}, "children"),
|
|
1505
1514
|
State("theme-store", "data"),
|
|
1506
|
-
State("session-id", "data")
|
|
1515
|
+
State("session-id", "data"),
|
|
1516
|
+
State("expanded-folders", "data")],
|
|
1507
1517
|
prevent_initial_call=True
|
|
1508
1518
|
)
|
|
1509
|
-
def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, children_styles, icon_styles, children_content, theme, session_id):
|
|
1519
|
+
def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, children_styles, icon_styles, children_content, theme, session_id, expanded_folders):
|
|
1510
1520
|
"""Toggle folder expansion and lazy load contents if needed."""
|
|
1511
1521
|
ctx = callback_context
|
|
1512
1522
|
if not ctx.triggered or not any(n_clicks):
|
|
1513
1523
|
raise PreventUpdate
|
|
1514
1524
|
|
|
1515
1525
|
colors = get_colors(theme or "light")
|
|
1526
|
+
expanded_folders = expanded_folders or []
|
|
1516
1527
|
|
|
1517
1528
|
# Get workspace for this session (virtual or physical)
|
|
1518
1529
|
workspace_root = get_workspace_for_session(session_id)
|
|
@@ -1538,6 +1549,9 @@ def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, chil
|
|
|
1538
1549
|
new_icon_styles = []
|
|
1539
1550
|
new_children_content = []
|
|
1540
1551
|
|
|
1552
|
+
# Track whether we're expanding or collapsing the clicked folder
|
|
1553
|
+
will_expand = None
|
|
1554
|
+
|
|
1541
1555
|
# Process all folder-children elements
|
|
1542
1556
|
for i, child_id in enumerate(children_ids):
|
|
1543
1557
|
path = child_id["path"]
|
|
@@ -1547,6 +1561,7 @@ def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, chil
|
|
|
1547
1561
|
if path == clicked_path:
|
|
1548
1562
|
# Toggle this folder
|
|
1549
1563
|
is_expanded = current_style.get("display") != "none"
|
|
1564
|
+
will_expand = not is_expanded
|
|
1550
1565
|
new_children_styles.append({"display": "none" if is_expanded else "block"})
|
|
1551
1566
|
|
|
1552
1567
|
# If expanding and content is just "Loading...", load the actual contents
|
|
@@ -1560,7 +1575,8 @@ def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, chil
|
|
|
1560
1575
|
folder_items = load_folder_contents(folder_rel_path, workspace_root)
|
|
1561
1576
|
loaded_content = render_file_tree(folder_items, colors, STYLES,
|
|
1562
1577
|
level=folder_rel_path.count("/") + folder_rel_path.count("\\") + 1,
|
|
1563
|
-
parent_path=folder_rel_path
|
|
1578
|
+
parent_path=folder_rel_path,
|
|
1579
|
+
expanded_folders=expanded_folders)
|
|
1564
1580
|
new_children_content.append(loaded_content if loaded_content else current_content)
|
|
1565
1581
|
except Exception as e:
|
|
1566
1582
|
print(f"Error loading folder {folder_rel_path}: {e}")
|
|
@@ -1597,14 +1613,23 @@ def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, chil
|
|
|
1597
1613
|
else:
|
|
1598
1614
|
new_icon_styles.append(current_icon_style)
|
|
1599
1615
|
|
|
1600
|
-
|
|
1616
|
+
# Update expanded folders list
|
|
1617
|
+
new_expanded_folders = list(expanded_folders)
|
|
1618
|
+
if will_expand is not None:
|
|
1619
|
+
if will_expand and clicked_path not in new_expanded_folders:
|
|
1620
|
+
new_expanded_folders.append(clicked_path)
|
|
1621
|
+
elif not will_expand and clicked_path in new_expanded_folders:
|
|
1622
|
+
new_expanded_folders.remove(clicked_path)
|
|
1623
|
+
|
|
1624
|
+
return new_children_styles, new_icon_styles, new_children_content, new_expanded_folders
|
|
1601
1625
|
|
|
1602
1626
|
|
|
1603
1627
|
# Enter folder callback - triggered by double-clicking folder name (changes workspace root)
|
|
1604
1628
|
@app.callback(
|
|
1605
1629
|
[Output("current-workspace-path", "data"),
|
|
1606
1630
|
Output("workspace-breadcrumb", "children"),
|
|
1607
|
-
Output("file-tree", "children", allow_duplicate=True)
|
|
1631
|
+
Output("file-tree", "children", allow_duplicate=True),
|
|
1632
|
+
Output("expanded-folders", "data", allow_duplicate=True)],
|
|
1608
1633
|
[Input({"type": "folder-select", "path": ALL}, "n_clicks"),
|
|
1609
1634
|
Input("breadcrumb-root", "n_clicks"),
|
|
1610
1635
|
Input({"type": "breadcrumb-segment", "index": ALL}, "n_clicks")],
|
|
@@ -1720,13 +1745,13 @@ def enter_folder(folder_clicks, root_clicks, breadcrumb_clicks, folder_ids, fold
|
|
|
1720
1745
|
else:
|
|
1721
1746
|
workspace_full_path = workspace_root / new_path if new_path else workspace_root
|
|
1722
1747
|
|
|
1723
|
-
# Render new file tree
|
|
1748
|
+
# Render new file tree (reset expanded folders when navigating)
|
|
1724
1749
|
file_tree = render_file_tree(
|
|
1725
1750
|
build_file_tree(workspace_full_path, workspace_full_path),
|
|
1726
1751
|
colors, STYLES
|
|
1727
1752
|
)
|
|
1728
1753
|
|
|
1729
|
-
return new_path, breadcrumb_children, file_tree
|
|
1754
|
+
return new_path, breadcrumb_children, file_tree, [] # Reset expanded folders
|
|
1730
1755
|
|
|
1731
1756
|
|
|
1732
1757
|
# File click - open modal
|
|
@@ -1805,23 +1830,207 @@ def open_file_modal(all_n_clicks, all_ids, click_tracker, theme, session_id):
|
|
|
1805
1830
|
colors = get_colors(theme or "light")
|
|
1806
1831
|
content, is_text, error = read_file_content(workspace_root, file_path)
|
|
1807
1832
|
filename = Path(file_path).name
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1833
|
+
file_ext = Path(file_path).suffix.lower()
|
|
1834
|
+
|
|
1835
|
+
# Define file type categories for binary previews
|
|
1836
|
+
image_exts = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp'}
|
|
1837
|
+
pdf_exts = {'.pdf'}
|
|
1838
|
+
|
|
1839
|
+
# Check for binary preview types first
|
|
1840
|
+
if file_ext in image_exts | pdf_exts:
|
|
1841
|
+
b64, _, mime = get_file_download_data(workspace_root, file_path)
|
|
1842
|
+
if b64:
|
|
1843
|
+
data_url = f"data:{mime};base64,{b64}"
|
|
1844
|
+
|
|
1845
|
+
if file_ext in image_exts:
|
|
1846
|
+
# Image preview
|
|
1847
|
+
modal_content = html.Div([
|
|
1848
|
+
html.Img(
|
|
1849
|
+
src=data_url,
|
|
1850
|
+
style={
|
|
1851
|
+
"maxWidth": "100%",
|
|
1852
|
+
"maxHeight": "80vh",
|
|
1853
|
+
"display": "block",
|
|
1854
|
+
"margin": "0 auto",
|
|
1855
|
+
"borderRadius": "4px",
|
|
1856
|
+
}
|
|
1857
|
+
)
|
|
1858
|
+
], style={"textAlign": "center"})
|
|
1859
|
+
|
|
1860
|
+
elif file_ext in pdf_exts:
|
|
1861
|
+
# PDF preview via embed
|
|
1862
|
+
modal_content = html.Embed(
|
|
1863
|
+
src=data_url,
|
|
1864
|
+
type="application/pdf",
|
|
1865
|
+
style={
|
|
1866
|
+
"width": "100%",
|
|
1867
|
+
"height": "80vh",
|
|
1868
|
+
"borderRadius": "4px",
|
|
1869
|
+
}
|
|
1870
|
+
)
|
|
1871
|
+
else:
|
|
1872
|
+
# Failed to read binary file
|
|
1873
|
+
modal_content = html.Div([
|
|
1874
|
+
html.P("Failed to load file preview", style={
|
|
1875
|
+
"color": colors["text_muted"],
|
|
1876
|
+
"textAlign": "center",
|
|
1877
|
+
"padding": "40px",
|
|
1878
|
+
}),
|
|
1879
|
+
html.P("Click Download to save the file.", style={
|
|
1880
|
+
"color": colors["text_muted"],
|
|
1881
|
+
"textAlign": "center",
|
|
1882
|
+
"fontSize": "13px",
|
|
1883
|
+
})
|
|
1884
|
+
])
|
|
1885
|
+
|
|
1886
|
+
elif is_text and content:
|
|
1887
|
+
# HTML files get rendered preview
|
|
1888
|
+
if file_ext in ('.html', '.htm'):
|
|
1889
|
+
modal_content = html.Div([
|
|
1890
|
+
# Tab buttons for switching views
|
|
1891
|
+
html.Div([
|
|
1892
|
+
html.Button("Preview", id="html-preview-tab", n_clicks=0,
|
|
1893
|
+
className="html-tab-btn html-tab-active",
|
|
1894
|
+
style={"marginRight": "8px", "padding": "6px 12px", "border": "none",
|
|
1895
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
1896
|
+
"background": colors["accent"], "color": "#fff"}),
|
|
1897
|
+
html.Button("Source", id="html-source-tab", n_clicks=0,
|
|
1898
|
+
className="html-tab-btn",
|
|
1899
|
+
style={"padding": "6px 12px", "border": f"1px solid {colors['border']}",
|
|
1900
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
1901
|
+
"background": "transparent", "color": colors["text_primary"]}),
|
|
1902
|
+
], style={"marginBottom": "12px", "display": "flex"}),
|
|
1903
|
+
# Preview iframe (default visible)
|
|
1904
|
+
html.Iframe(
|
|
1905
|
+
srcDoc=content,
|
|
1906
|
+
style={
|
|
1907
|
+
"width": "100%",
|
|
1908
|
+
"height": "80vh",
|
|
1909
|
+
"border": f"1px solid {colors['border']}",
|
|
1910
|
+
"borderRadius": "4px",
|
|
1911
|
+
"background": "#fff",
|
|
1912
|
+
},
|
|
1913
|
+
id="html-preview-frame"
|
|
1914
|
+
),
|
|
1915
|
+
# Source code (hidden by default)
|
|
1916
|
+
html.Pre(
|
|
1917
|
+
content,
|
|
1918
|
+
id="html-source-code",
|
|
1919
|
+
style={
|
|
1920
|
+
"display": "none",
|
|
1921
|
+
"background": colors["bg_tertiary"],
|
|
1922
|
+
"padding": "16px",
|
|
1923
|
+
"fontSize": "12px",
|
|
1924
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
1925
|
+
"overflow": "auto",
|
|
1926
|
+
"maxHeight": "80vh",
|
|
1927
|
+
"whiteSpace": "pre-wrap",
|
|
1928
|
+
"wordBreak": "break-word",
|
|
1929
|
+
"margin": "0",
|
|
1930
|
+
"color": colors["text_primary"],
|
|
1931
|
+
"border": f"1px solid {colors['border']}",
|
|
1932
|
+
"borderRadius": "4px",
|
|
1933
|
+
}
|
|
1934
|
+
)
|
|
1935
|
+
])
|
|
1936
|
+
elif file_ext == '.json':
|
|
1937
|
+
# Try to parse as Plotly JSON figure
|
|
1938
|
+
plotly_figure = None
|
|
1939
|
+
try:
|
|
1940
|
+
data = json.loads(content)
|
|
1941
|
+
# Check if it looks like a Plotly figure (has 'data' key with list)
|
|
1942
|
+
if isinstance(data, dict) and 'data' in data and isinstance(data['data'], list):
|
|
1943
|
+
plotly_figure = data
|
|
1944
|
+
except (json.JSONDecodeError, KeyError):
|
|
1945
|
+
pass
|
|
1946
|
+
|
|
1947
|
+
if plotly_figure:
|
|
1948
|
+
# Render as interactive Plotly chart with source toggle
|
|
1949
|
+
modal_content = html.Div([
|
|
1950
|
+
# Tab buttons for switching views
|
|
1951
|
+
html.Div([
|
|
1952
|
+
html.Button("Chart", id="html-preview-tab", n_clicks=0,
|
|
1953
|
+
className="html-tab-btn html-tab-active",
|
|
1954
|
+
style={"marginRight": "8px", "padding": "6px 12px", "border": "none",
|
|
1955
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
1956
|
+
"background": colors["accent"], "color": "#fff"}),
|
|
1957
|
+
html.Button("JSON", id="html-source-tab", n_clicks=0,
|
|
1958
|
+
className="html-tab-btn",
|
|
1959
|
+
style={"padding": "6px 12px", "border": f"1px solid {colors['border']}",
|
|
1960
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
1961
|
+
"background": "transparent", "color": colors["text_primary"]}),
|
|
1962
|
+
], style={"marginBottom": "12px", "display": "flex"}),
|
|
1963
|
+
# Plotly chart (default visible)
|
|
1964
|
+
html.Div([
|
|
1965
|
+
dcc.Graph(
|
|
1966
|
+
figure=plotly_figure,
|
|
1967
|
+
style={"height": "75vh"},
|
|
1968
|
+
config={"displayModeBar": True, "responsive": True}
|
|
1969
|
+
)
|
|
1970
|
+
], id="html-preview-frame", style={
|
|
1971
|
+
"border": f"1px solid {colors['border']}",
|
|
1972
|
+
"borderRadius": "4px",
|
|
1973
|
+
"background": "#fff",
|
|
1974
|
+
}),
|
|
1975
|
+
# JSON source (hidden by default)
|
|
1976
|
+
html.Pre(
|
|
1977
|
+
json.dumps(plotly_figure, indent=2),
|
|
1978
|
+
id="html-source-code",
|
|
1979
|
+
style={
|
|
1980
|
+
"display": "none",
|
|
1981
|
+
"background": colors["bg_tertiary"],
|
|
1982
|
+
"padding": "16px",
|
|
1983
|
+
"fontSize": "12px",
|
|
1984
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
1985
|
+
"overflow": "auto",
|
|
1986
|
+
"maxHeight": "80vh",
|
|
1987
|
+
"whiteSpace": "pre-wrap",
|
|
1988
|
+
"wordBreak": "break-word",
|
|
1989
|
+
"margin": "0",
|
|
1990
|
+
"color": colors["text_primary"],
|
|
1991
|
+
"border": f"1px solid {colors['border']}",
|
|
1992
|
+
"borderRadius": "4px",
|
|
1993
|
+
}
|
|
1994
|
+
)
|
|
1995
|
+
])
|
|
1996
|
+
else:
|
|
1997
|
+
# Regular JSON - show formatted
|
|
1998
|
+
try:
|
|
1999
|
+
formatted = json.dumps(json.loads(content), indent=2)
|
|
2000
|
+
except json.JSONDecodeError:
|
|
2001
|
+
formatted = content
|
|
2002
|
+
modal_content = html.Pre(
|
|
2003
|
+
formatted,
|
|
2004
|
+
style={
|
|
2005
|
+
"background": colors["bg_tertiary"],
|
|
2006
|
+
"padding": "16px",
|
|
2007
|
+
"fontSize": "12px",
|
|
2008
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
2009
|
+
"overflow": "auto",
|
|
2010
|
+
"maxHeight": "80vh",
|
|
2011
|
+
"whiteSpace": "pre-wrap",
|
|
2012
|
+
"wordBreak": "break-word",
|
|
2013
|
+
"margin": "0",
|
|
2014
|
+
"color": colors["text_primary"],
|
|
2015
|
+
}
|
|
2016
|
+
)
|
|
2017
|
+
else:
|
|
2018
|
+
# Regular text files
|
|
2019
|
+
modal_content = html.Pre(
|
|
2020
|
+
content,
|
|
2021
|
+
style={
|
|
2022
|
+
"background": colors["bg_tertiary"],
|
|
2023
|
+
"padding": "16px",
|
|
2024
|
+
"fontSize": "12px",
|
|
2025
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
2026
|
+
"overflow": "auto",
|
|
2027
|
+
"maxHeight": "80vh",
|
|
2028
|
+
"whiteSpace": "pre-wrap",
|
|
2029
|
+
"wordBreak": "break-word",
|
|
2030
|
+
"margin": "0",
|
|
2031
|
+
"color": colors["text_primary"],
|
|
2032
|
+
}
|
|
2033
|
+
)
|
|
1825
2034
|
else:
|
|
1826
2035
|
modal_content = html.Div([
|
|
1827
2036
|
html.P(error or "Cannot display file", style={
|
|
@@ -1870,6 +2079,71 @@ def download_from_modal(n_clicks, file_path, session_id):
|
|
|
1870
2079
|
return dict(content=b64, filename=filename, base64=True, type=mime)
|
|
1871
2080
|
|
|
1872
2081
|
|
|
2082
|
+
# HTML preview/source tab switching
|
|
2083
|
+
@app.callback(
|
|
2084
|
+
[Output("html-preview-frame", "style"),
|
|
2085
|
+
Output("html-source-code", "style"),
|
|
2086
|
+
Output("html-preview-tab", "style"),
|
|
2087
|
+
Output("html-source-tab", "style")],
|
|
2088
|
+
[Input("html-preview-tab", "n_clicks"),
|
|
2089
|
+
Input("html-source-tab", "n_clicks")],
|
|
2090
|
+
[State("theme-store", "data")],
|
|
2091
|
+
prevent_initial_call=True
|
|
2092
|
+
)
|
|
2093
|
+
def toggle_html_view(preview_clicks, source_clicks, theme):
|
|
2094
|
+
"""Toggle between HTML preview and source code view."""
|
|
2095
|
+
ctx = callback_context
|
|
2096
|
+
if not ctx.triggered:
|
|
2097
|
+
raise PreventUpdate
|
|
2098
|
+
|
|
2099
|
+
colors = get_colors(theme or "light")
|
|
2100
|
+
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
2101
|
+
|
|
2102
|
+
# Base styles
|
|
2103
|
+
preview_frame_style = {
|
|
2104
|
+
"width": "100%",
|
|
2105
|
+
"height": "80vh",
|
|
2106
|
+
"border": f"1px solid {colors['border']}",
|
|
2107
|
+
"borderRadius": "4px",
|
|
2108
|
+
"background": "#fff",
|
|
2109
|
+
}
|
|
2110
|
+
source_code_style = {
|
|
2111
|
+
"background": colors["bg_tertiary"],
|
|
2112
|
+
"padding": "16px",
|
|
2113
|
+
"fontSize": "12px",
|
|
2114
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
2115
|
+
"overflow": "auto",
|
|
2116
|
+
"maxHeight": "80vh",
|
|
2117
|
+
"whiteSpace": "pre-wrap",
|
|
2118
|
+
"wordBreak": "break-word",
|
|
2119
|
+
"margin": "0",
|
|
2120
|
+
"color": colors["text_primary"],
|
|
2121
|
+
"border": f"1px solid {colors['border']}",
|
|
2122
|
+
"borderRadius": "4px",
|
|
2123
|
+
}
|
|
2124
|
+
active_btn_style = {
|
|
2125
|
+
"marginRight": "8px", "padding": "6px 12px", "border": "none",
|
|
2126
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
2127
|
+
"background": colors["accent"], "color": "#fff"
|
|
2128
|
+
}
|
|
2129
|
+
inactive_btn_style = {
|
|
2130
|
+
"padding": "6px 12px", "border": f"1px solid {colors['border']}",
|
|
2131
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
2132
|
+
"background": "transparent", "color": colors["text_primary"]
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if triggered_id == "html-source-tab":
|
|
2136
|
+
# Show source, hide preview
|
|
2137
|
+
preview_frame_style["display"] = "none"
|
|
2138
|
+
source_code_style["display"] = "block"
|
|
2139
|
+
return preview_frame_style, source_code_style, {**inactive_btn_style, "marginRight": "8px"}, active_btn_style
|
|
2140
|
+
else:
|
|
2141
|
+
# Show preview, hide source (default)
|
|
2142
|
+
preview_frame_style["display"] = "block"
|
|
2143
|
+
source_code_style["display"] = "none"
|
|
2144
|
+
return preview_frame_style, source_code_style, active_btn_style, {**inactive_btn_style}
|
|
2145
|
+
|
|
2146
|
+
|
|
1873
2147
|
# Open terminal
|
|
1874
2148
|
@app.callback(
|
|
1875
2149
|
Output("open-terminal-btn", "n_clicks"),
|
|
@@ -1922,13 +2196,15 @@ def open_terminal(n_clicks):
|
|
|
1922
2196
|
[State("current-workspace-path", "data"),
|
|
1923
2197
|
State("theme-store", "data"),
|
|
1924
2198
|
State("collapsed-canvas-items", "data"),
|
|
1925
|
-
State("session-id", "data")
|
|
2199
|
+
State("session-id", "data"),
|
|
2200
|
+
State("expanded-folders", "data")],
|
|
1926
2201
|
prevent_initial_call=True
|
|
1927
2202
|
)
|
|
1928
|
-
def refresh_sidebar(n_clicks, current_workspace, theme, collapsed_ids, session_id):
|
|
2203
|
+
def refresh_sidebar(n_clicks, current_workspace, theme, collapsed_ids, session_id, expanded_folders):
|
|
1929
2204
|
"""Refresh both file tree and canvas content."""
|
|
1930
2205
|
colors = get_colors(theme or "light")
|
|
1931
2206
|
collapsed_ids = collapsed_ids or []
|
|
2207
|
+
expanded_folders = expanded_folders or []
|
|
1932
2208
|
|
|
1933
2209
|
# Get workspace for this session (virtual or physical)
|
|
1934
2210
|
workspace_root = get_workspace_for_session(session_id)
|
|
@@ -1939,8 +2215,8 @@ def refresh_sidebar(n_clicks, current_workspace, theme, collapsed_ids, session_i
|
|
|
1939
2215
|
else:
|
|
1940
2216
|
current_workspace_dir = workspace_root / current_workspace if current_workspace else workspace_root
|
|
1941
2217
|
|
|
1942
|
-
# Refresh file tree for current workspace
|
|
1943
|
-
file_tree = render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES)
|
|
2218
|
+
# Refresh file tree for current workspace, preserving expanded folders
|
|
2219
|
+
file_tree = render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders)
|
|
1944
2220
|
|
|
1945
2221
|
# Re-render canvas from current in-memory state (don't reload from file)
|
|
1946
2222
|
# This preserves canvas items that may not have been exported to .canvas/canvas.md yet
|
|
@@ -1960,15 +2236,17 @@ def refresh_sidebar(n_clicks, current_workspace, theme, collapsed_ids, session_i
|
|
|
1960
2236
|
[State("file-upload-sidebar", "filename"),
|
|
1961
2237
|
State("current-workspace-path", "data"),
|
|
1962
2238
|
State("theme-store", "data"),
|
|
1963
|
-
State("session-id", "data")
|
|
2239
|
+
State("session-id", "data"),
|
|
2240
|
+
State("expanded-folders", "data")],
|
|
1964
2241
|
prevent_initial_call=True
|
|
1965
2242
|
)
|
|
1966
|
-
def handle_sidebar_upload(contents, filenames, current_workspace, theme, session_id):
|
|
2243
|
+
def handle_sidebar_upload(contents, filenames, current_workspace, theme, session_id, expanded_folders):
|
|
1967
2244
|
"""Handle file uploads from sidebar button to current workspace."""
|
|
1968
2245
|
if not contents:
|
|
1969
2246
|
raise PreventUpdate
|
|
1970
2247
|
|
|
1971
2248
|
colors = get_colors(theme or "light")
|
|
2249
|
+
expanded_folders = expanded_folders or []
|
|
1972
2250
|
|
|
1973
2251
|
# Get workspace for this session (virtual or physical)
|
|
1974
2252
|
workspace_root = get_workspace_for_session(session_id)
|
|
@@ -1991,7 +2269,7 @@ def handle_sidebar_upload(contents, filenames, current_workspace, theme, session
|
|
|
1991
2269
|
except Exception as e:
|
|
1992
2270
|
print(f"Upload error: {e}")
|
|
1993
2271
|
|
|
1994
|
-
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES)
|
|
2272
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders)
|
|
1995
2273
|
|
|
1996
2274
|
|
|
1997
2275
|
# Create folder modal - open
|
|
@@ -2034,15 +2312,17 @@ def toggle_create_folder_modal(open_clicks, cancel_clicks, confirm_clicks, is_op
|
|
|
2034
2312
|
[State("new-folder-name", "value"),
|
|
2035
2313
|
State("current-workspace-path", "data"),
|
|
2036
2314
|
State("theme-store", "data"),
|
|
2037
|
-
State("session-id", "data")
|
|
2315
|
+
State("session-id", "data"),
|
|
2316
|
+
State("expanded-folders", "data")],
|
|
2038
2317
|
prevent_initial_call=True
|
|
2039
2318
|
)
|
|
2040
|
-
def create_folder(n_clicks, folder_name, current_workspace, theme, session_id):
|
|
2319
|
+
def create_folder(n_clicks, folder_name, current_workspace, theme, session_id, expanded_folders):
|
|
2041
2320
|
"""Create a new folder in the current workspace directory."""
|
|
2042
2321
|
if not n_clicks:
|
|
2043
2322
|
raise PreventUpdate
|
|
2044
2323
|
|
|
2045
2324
|
colors = get_colors(theme or "light")
|
|
2325
|
+
expanded_folders = expanded_folders or []
|
|
2046
2326
|
|
|
2047
2327
|
if not folder_name or not folder_name.strip():
|
|
2048
2328
|
return no_update, "Please enter a folder name", no_update
|
|
@@ -2070,7 +2350,7 @@ def create_folder(n_clicks, folder_name, current_workspace, theme, session_id):
|
|
|
2070
2350
|
|
|
2071
2351
|
try:
|
|
2072
2352
|
folder_path.mkdir(parents=True, exist_ok=False)
|
|
2073
|
-
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES), "", ""
|
|
2353
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders), "", ""
|
|
2074
2354
|
except Exception as e:
|
|
2075
2355
|
return no_update, f"Error creating folder: {e}", no_update
|
|
2076
2356
|
|
|
@@ -2156,15 +2436,17 @@ def update_canvas_content(n_intervals, view_value, theme, collapsed_ids, session
|
|
|
2156
2436
|
[State("current-workspace-path", "data"),
|
|
2157
2437
|
State("theme-store", "data"),
|
|
2158
2438
|
State("session-id", "data"),
|
|
2159
|
-
State("sidebar-view-toggle", "value")
|
|
2439
|
+
State("sidebar-view-toggle", "value"),
|
|
2440
|
+
State("expanded-folders", "data")],
|
|
2160
2441
|
prevent_initial_call=True
|
|
2161
2442
|
)
|
|
2162
|
-
def poll_file_tree_update(n_intervals, current_workspace, theme, session_id, view_value):
|
|
2443
|
+
def poll_file_tree_update(n_intervals, current_workspace, theme, session_id, view_value, expanded_folders):
|
|
2163
2444
|
"""Refresh file tree during agent execution to show newly created files.
|
|
2164
2445
|
|
|
2165
2446
|
This callback runs on each poll interval and refreshes the file tree
|
|
2166
2447
|
so that files created by the agent are visible in real-time.
|
|
2167
2448
|
Only updates when viewing files (not canvas).
|
|
2449
|
+
Preserves expanded folder state across refreshes.
|
|
2168
2450
|
"""
|
|
2169
2451
|
# Only refresh when viewing files panel
|
|
2170
2452
|
if view_value != "files":
|
|
@@ -2180,6 +2462,7 @@ def poll_file_tree_update(n_intervals, current_workspace, theme, session_id, vie
|
|
|
2180
2462
|
raise PreventUpdate
|
|
2181
2463
|
|
|
2182
2464
|
colors = get_colors(theme or "light")
|
|
2465
|
+
expanded_folders = expanded_folders or []
|
|
2183
2466
|
|
|
2184
2467
|
# Get workspace for this session (virtual or physical)
|
|
2185
2468
|
workspace_root = get_workspace_for_session(session_id)
|
|
@@ -2190,8 +2473,8 @@ def poll_file_tree_update(n_intervals, current_workspace, theme, session_id, vie
|
|
|
2190
2473
|
else:
|
|
2191
2474
|
current_workspace_dir = workspace_root / current_workspace if current_workspace else workspace_root
|
|
2192
2475
|
|
|
2193
|
-
# Refresh file tree
|
|
2194
|
-
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES)
|
|
2476
|
+
# Refresh file tree, preserving expanded folder state
|
|
2477
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders)
|
|
2195
2478
|
|
|
2196
2479
|
|
|
2197
2480
|
# Open clear canvas confirmation modal
|
cowork_dash/canvas.py
CHANGED
|
@@ -13,6 +13,14 @@ from pathlib import Path
|
|
|
13
13
|
from typing import Any, Dict, List, Optional, Union
|
|
14
14
|
from datetime import datetime
|
|
15
15
|
|
|
16
|
+
# Early pandas import to prevent circular import issues with Plotly's JSON serializer.
|
|
17
|
+
# Plotly lazily imports pandas and checks `obj is pd.NaT` which fails if pandas
|
|
18
|
+
# is partially initialized due to concurrent imports.
|
|
19
|
+
try:
|
|
20
|
+
import pandas
|
|
21
|
+
except (ImportError, AttributeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
16
24
|
from .virtual_fs import VirtualFilesystem, VirtualPath
|
|
17
25
|
|
|
18
26
|
|
cowork_dash/file_utils.py
CHANGED
|
@@ -136,15 +136,17 @@ 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 = "") -> List:
|
|
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
140
|
"""Render file tree with collapsible folders using CSS classes for theming."""
|
|
141
141
|
components = []
|
|
142
142
|
indent = level * 15 # Scaled up indent
|
|
143
|
+
expanded_folders = expanded_folders or []
|
|
143
144
|
|
|
144
145
|
for item in items:
|
|
145
146
|
if item["type"] == "folder":
|
|
146
147
|
folder_id = item["path"].replace("/", "_").replace("\\", "_")
|
|
147
148
|
children = item.get("children", [])
|
|
149
|
+
is_expanded = folder_id in expanded_folders
|
|
148
150
|
|
|
149
151
|
# Folder header with expand icon and selectable name
|
|
150
152
|
components.append(
|
|
@@ -160,6 +162,7 @@ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int =
|
|
|
160
162
|
"transition": "transform 0.15s",
|
|
161
163
|
"display": "inline-block",
|
|
162
164
|
"padding": "2px",
|
|
165
|
+
"transform": "rotate(90deg)" if is_expanded else "rotate(0deg)",
|
|
163
166
|
}
|
|
164
167
|
),
|
|
165
168
|
# Folder name (clickable for selection)
|
|
@@ -196,7 +199,7 @@ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int =
|
|
|
196
199
|
|
|
197
200
|
if children:
|
|
198
201
|
# Children are loaded, render them
|
|
199
|
-
child_content = render_file_tree(children, colors, styles, level + 1, item["path"])
|
|
202
|
+
child_content = render_file_tree(children, colors, styles, level + 1, item["path"], expanded_folders)
|
|
200
203
|
elif not has_children:
|
|
201
204
|
# Folder is known to be empty
|
|
202
205
|
child_content = [
|
|
@@ -226,7 +229,7 @@ def render_file_tree(items: List[Dict], colors: Dict, styles: Dict, level: int =
|
|
|
226
229
|
html.Div(
|
|
227
230
|
child_content,
|
|
228
231
|
id={"type": "folder-children", "path": folder_id},
|
|
229
|
-
style={"display": "
|
|
232
|
+
style={"display": "block" if is_expanded else "none"}
|
|
230
233
|
)
|
|
231
234
|
)
|
|
232
235
|
else:
|
|
@@ -288,11 +291,25 @@ def get_file_download_data(
|
|
|
288
291
|
# Determine MIME type
|
|
289
292
|
ext = PurePosixPath(path).suffix.lower()
|
|
290
293
|
mime_types = {
|
|
294
|
+
# Text
|
|
291
295
|
".txt": "text/plain", ".py": "text/x-python", ".js": "text/javascript",
|
|
292
|
-
".json": "application/json", ".html": "text/html", ".
|
|
293
|
-
".
|
|
294
|
-
".
|
|
295
|
-
|
|
296
|
+
".json": "application/json", ".html": "text/html", ".htm": "text/html",
|
|
297
|
+
".css": "text/css", ".md": "text/markdown", ".csv": "text/csv",
|
|
298
|
+
".xml": "text/xml", ".yaml": "text/yaml", ".yml": "text/yaml",
|
|
299
|
+
# Images
|
|
300
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
301
|
+
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
302
|
+
".ico": "image/x-icon", ".bmp": "image/bmp",
|
|
303
|
+
# Video
|
|
304
|
+
".mp4": "video/mp4", ".webm": "video/webm", ".ogg": "video/ogg",
|
|
305
|
+
".mov": "video/quicktime",
|
|
306
|
+
# Audio
|
|
307
|
+
".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4",
|
|
308
|
+
".flac": "audio/flac",
|
|
309
|
+
# Documents
|
|
310
|
+
".pdf": "application/pdf",
|
|
311
|
+
# Archives
|
|
312
|
+
".zip": "application/zip",
|
|
296
313
|
}
|
|
297
314
|
mime = mime_types.get(ext, "application/octet-stream")
|
|
298
315
|
|
cowork_dash/tools.py
CHANGED
|
@@ -1,15 +1,61 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional
|
|
2
2
|
import sys
|
|
3
3
|
import io
|
|
4
|
+
import os
|
|
4
5
|
import traceback
|
|
5
6
|
import subprocess
|
|
6
7
|
import threading
|
|
7
|
-
|
|
8
|
+
import platform
|
|
9
|
+
from contextlib import redirect_stdout, redirect_stderr, contextmanager
|
|
8
10
|
|
|
9
11
|
from .config import WORKSPACE_ROOT, VIRTUAL_FS
|
|
10
12
|
from .canvas import parse_canvas_object, generate_canvas_id
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
# Memory limit for cell execution (in bytes)
|
|
16
|
+
# Default: 512 MB - can be overridden via environment variable
|
|
17
|
+
CELL_MEMORY_LIMIT_MB = int(os.environ.get("COWORK_CELL_MEMORY_LIMIT_MB", "512"))
|
|
18
|
+
CELL_MEMORY_LIMIT_BYTES = CELL_MEMORY_LIMIT_MB * 1024 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def memory_limit(max_bytes: int = CELL_MEMORY_LIMIT_BYTES):
|
|
23
|
+
"""Context manager to set memory limits for code execution on Linux.
|
|
24
|
+
|
|
25
|
+
On Linux, uses resource.setrlimit() to set soft memory limit.
|
|
26
|
+
On other platforms (macOS, Windows), this is a no-op as they don't
|
|
27
|
+
support RLIMIT_AS in the same way or at all.
|
|
28
|
+
|
|
29
|
+
The limit applies to the virtual address space (RLIMIT_AS) which
|
|
30
|
+
will cause MemoryError when exceeded rather than OOM kill.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
max_bytes: Maximum memory in bytes. Default from CELL_MEMORY_LIMIT_BYTES.
|
|
34
|
+
"""
|
|
35
|
+
if platform.system() != "Linux":
|
|
36
|
+
# Memory limits via resource module only work reliably on Linux
|
|
37
|
+
yield
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import resource
|
|
42
|
+
except ImportError:
|
|
43
|
+
yield
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Get current limits
|
|
47
|
+
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Set new soft limit (don't exceed hard limit)
|
|
51
|
+
new_soft = min(max_bytes, hard) if hard != resource.RLIM_INFINITY else max_bytes
|
|
52
|
+
resource.setrlimit(resource.RLIMIT_AS, (new_soft, hard))
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
# Restore original limits
|
|
56
|
+
resource.setrlimit(resource.RLIMIT_AS, (soft, hard))
|
|
57
|
+
|
|
58
|
+
|
|
13
59
|
# Thread-local storage for current session context
|
|
14
60
|
# This allows tools to know which session they're operating in
|
|
15
61
|
_tool_context = threading.local()
|
|
@@ -383,47 +429,54 @@ except (ImportError, AttributeError):
|
|
|
383
429
|
}
|
|
384
430
|
|
|
385
431
|
try:
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
432
|
+
# Apply memory limit on Linux to prevent OOM kills
|
|
433
|
+
with memory_limit():
|
|
434
|
+
# Try IPython first for better execution handling
|
|
435
|
+
ipython = self._get_ipython()
|
|
436
|
+
|
|
437
|
+
if ipython is not None:
|
|
438
|
+
# Use IPython's run_cell for magic commands support
|
|
439
|
+
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
|
|
440
|
+
exec_result = ipython.run_cell(cell["source"], store_history=True)
|
|
441
|
+
|
|
442
|
+
result["stdout"] = stdout_capture.getvalue()
|
|
443
|
+
result["stderr"] = stderr_capture.getvalue()
|
|
444
|
+
|
|
445
|
+
if exec_result.success:
|
|
446
|
+
if exec_result.result is not None:
|
|
447
|
+
result["result"] = repr(exec_result.result)
|
|
448
|
+
else:
|
|
449
|
+
if exec_result.error_in_exec:
|
|
450
|
+
result["error"] = str(exec_result.error_in_exec)
|
|
451
|
+
result["status"] = "error"
|
|
452
|
+
elif exec_result.error_before_exec:
|
|
453
|
+
result["error"] = str(exec_result.error_before_exec)
|
|
454
|
+
result["status"] = "error"
|
|
400
455
|
else:
|
|
401
|
-
if
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
result["stderr"] = stderr_capture.getvalue()
|
|
426
|
-
|
|
456
|
+
# Fallback to exec() if IPython is not available
|
|
457
|
+
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
|
|
458
|
+
# Compile to check for expression vs statement
|
|
459
|
+
code = cell["source"].strip()
|
|
460
|
+
|
|
461
|
+
# Try to evaluate as expression first (to get return value)
|
|
462
|
+
try:
|
|
463
|
+
# Check if it's a simple expression
|
|
464
|
+
compiled = compile(code, "<cell>", "eval")
|
|
465
|
+
exec_result = eval(compiled, self._namespace)
|
|
466
|
+
if exec_result is not None:
|
|
467
|
+
result["result"] = repr(exec_result)
|
|
468
|
+
except SyntaxError:
|
|
469
|
+
# It's a statement, execute it
|
|
470
|
+
exec(code, self._namespace)
|
|
471
|
+
|
|
472
|
+
result["stdout"] = stdout_capture.getvalue()
|
|
473
|
+
result["stderr"] = stderr_capture.getvalue()
|
|
474
|
+
|
|
475
|
+
except MemoryError:
|
|
476
|
+
result["error"] = f"MemoryError: Cell execution exceeded memory limit ({CELL_MEMORY_LIMIT_MB} MB). Try processing data in smaller chunks."
|
|
477
|
+
result["status"] = "error"
|
|
478
|
+
result["stdout"] = stdout_capture.getvalue()
|
|
479
|
+
result["stderr"] = stderr_capture.getvalue()
|
|
427
480
|
except Exception:
|
|
428
481
|
result["error"] = traceback.format_exc()
|
|
429
482
|
result["status"] = "error"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cowork-dash
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: AI Agent Web Interface with Filesystem and Canvas Visualization
|
|
5
5
|
Project-URL: Homepage, https://github.com/dkedar7/cowork-dash
|
|
6
6
|
Project-URL: Documentation, https://github.com/dkedar7/cowork-dash/blob/main/README.md
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
cowork_dash/__init__.py,sha256=37qBKl7g12Zos8GFukXLXligCdpD32e2qe9F8cd8Qdk,896
|
|
2
2
|
cowork_dash/__main__.py,sha256=CCM9VIkWuwh7hwVGNBBgCCbeVAcHj1soyBVXUaPgABk,131
|
|
3
3
|
cowork_dash/agent.py,sha256=JhCeQuMJj3VaQij2i8OPznKN6qGAixN-hakueiGfdt0,5851
|
|
4
|
-
cowork_dash/app.py,sha256=
|
|
4
|
+
cowork_dash/app.py,sha256=5Fd2MZThIovq3uxf2qNq1i9lEKIPtmvUHO3ikk3k4Mc,122674
|
|
5
5
|
cowork_dash/backends.py,sha256=YQE8f65o2qGxIIfvBITAS6krCLjl8D9UixW-pgbdgZk,15050
|
|
6
|
-
cowork_dash/canvas.py,sha256=
|
|
6
|
+
cowork_dash/canvas.py,sha256=sYOQ5WBLm29UazA5KO7j8jvIeQpx55LToz1o44o2U-k,17261
|
|
7
7
|
cowork_dash/cli.py,sha256=E7H9p21XcvuF6Kae4rjVVOavU7U8-TY0qFLHLPOtYWg,7125
|
|
8
8
|
cowork_dash/components.py,sha256=SlR0U1d0XThdcPZYButE_Fqt8MWPoLAgOTqtwfisvOQ,23776
|
|
9
9
|
cowork_dash/config.py,sha256=PoAxUbctyd_2JjH4eVffKhOPCEOvaRyPSzCmS4MFPi8,4570
|
|
10
|
-
cowork_dash/file_utils.py,sha256=
|
|
10
|
+
cowork_dash/file_utils.py,sha256=mhy7gZFxwd1YN28C8jxYrtTCRLVDx4JVqELN-KCdtu8,13368
|
|
11
11
|
cowork_dash/layout.py,sha256=6K6O7rxnYxcrIzMOQYtyth12wRGcZ_l8qKaZFvbxvRo,16634
|
|
12
|
-
cowork_dash/tools.py,sha256=
|
|
12
|
+
cowork_dash/tools.py,sha256=AOzO4_dl_mvTKr9bL0ibg0BUD53axm_Nfnth7io8bl0,35357
|
|
13
13
|
cowork_dash/virtual_fs.py,sha256=PAAdRiMkxgJ4xPpGUyAUd69KbH-nFlt-FjldfB1FrQ4,16296
|
|
14
14
|
cowork_dash/assets/app.js,sha256=Fo_tgFat9eXtpvrptLHmS0qAs04pfF0IaQYiT-Tohm8,8768
|
|
15
15
|
cowork_dash/assets/favicon.ico,sha256=IiP0rVr0m-TBGGmCY88SyFC14drNwDhLRqb0n2ZufKk,54252
|
|
16
16
|
cowork_dash/assets/favicon.svg,sha256=MdT50APCvIlWh3HSwW5SNXYWB3q_wKfuLP-JV53SnKg,1065
|
|
17
17
|
cowork_dash/assets/styles.css,sha256=w4D9jAneSUjCZxdXu1tIbbKPt_HnNF0nax4hy9liOHw,24095
|
|
18
|
-
cowork_dash-0.1.
|
|
19
|
-
cowork_dash-0.1.
|
|
20
|
-
cowork_dash-0.1.
|
|
21
|
-
cowork_dash-0.1.
|
|
22
|
-
cowork_dash-0.1.
|
|
18
|
+
cowork_dash-0.1.9.dist-info/METADATA,sha256=Y1_ieezAgbVMwQoy7HovfZ9696BZvX3QZoXNeXXYAuE,6719
|
|
19
|
+
cowork_dash-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
cowork_dash-0.1.9.dist-info/entry_points.txt,sha256=lL_9XJINiky3nh13tLqWd61LitKbbyh085ROATH9fck,53
|
|
21
|
+
cowork_dash-0.1.9.dist-info/licenses/LICENSE,sha256=2SFXFfIa_c_g_uwY0JApQDXI1mWqEfJeG87Pn4ehLMQ,1072
|
|
22
|
+
cowork_dash-0.1.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|