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/app.py
CHANGED
|
@@ -35,7 +35,7 @@ from .canvas import export_canvas_to_markdown, load_canvas_from_markdown
|
|
|
35
35
|
from .file_utils import build_file_tree, render_file_tree, read_file_content, get_file_download_data, load_folder_contents
|
|
36
36
|
from .components import (
|
|
37
37
|
format_message, format_loading, format_thinking, format_todos_inline, render_canvas_items, format_tool_calls_inline,
|
|
38
|
-
format_interrupt
|
|
38
|
+
format_interrupt, extract_display_inline_results, render_display_inline_result, extract_thinking_from_tool_calls
|
|
39
39
|
)
|
|
40
40
|
from .layout import create_layout as create_layout_component
|
|
41
41
|
from .virtual_fs import get_session_manager
|
|
@@ -251,6 +251,7 @@ _agent_state = {
|
|
|
251
251
|
"thinking": "",
|
|
252
252
|
"todos": [],
|
|
253
253
|
"tool_calls": [], # Current turn's tool calls (reset each turn)
|
|
254
|
+
"display_inline_items": [], # Items pushed by display_inline tool (bypasses LangGraph)
|
|
254
255
|
"canvas": load_canvas_from_markdown(WORKSPACE_ROOT) if not USE_VIRTUAL_FS else [], # Load from canvas.md if exists (physical FS only)
|
|
255
256
|
"response": "",
|
|
256
257
|
"error": None,
|
|
@@ -258,6 +259,7 @@ _agent_state = {
|
|
|
258
259
|
"last_update": time.time(),
|
|
259
260
|
"start_time": None, # Track when agent started for response time calculation
|
|
260
261
|
"stop_requested": False, # Flag to request agent stop
|
|
262
|
+
"stop_event": None, # Threading event for immediate stop signaling
|
|
261
263
|
}
|
|
262
264
|
_agent_state_lock = threading.Lock()
|
|
263
265
|
|
|
@@ -275,11 +277,13 @@ def _get_default_agent_state() -> Dict[str, Any]:
|
|
|
275
277
|
"thinking": "",
|
|
276
278
|
"todos": [],
|
|
277
279
|
"tool_calls": [],
|
|
280
|
+
"display_inline_items": [], # Items pushed by display_inline tool (bypasses LangGraph)
|
|
278
281
|
"canvas": [],
|
|
279
282
|
"response": "",
|
|
280
283
|
"error": None,
|
|
281
284
|
"interrupt": None,
|
|
282
285
|
"last_update": time.time(),
|
|
286
|
+
"stop_event": None, # Threading event for immediate stop signaling
|
|
283
287
|
"start_time": None,
|
|
284
288
|
"stop_requested": False,
|
|
285
289
|
}
|
|
@@ -323,7 +327,9 @@ def _get_session_state_lock() -> threading.Lock:
|
|
|
323
327
|
|
|
324
328
|
|
|
325
329
|
def request_agent_stop(session_id: Optional[str] = None):
|
|
326
|
-
"""Request the agent to stop execution.
|
|
330
|
+
"""Request the agent to stop execution immediately.
|
|
331
|
+
|
|
332
|
+
Sets the stop_requested flag and signals the stop_event for immediate interruption.
|
|
327
333
|
|
|
328
334
|
Args:
|
|
329
335
|
session_id: Session ID for virtual FS mode, None for physical FS mode.
|
|
@@ -333,10 +339,16 @@ def request_agent_stop(session_id: Optional[str] = None):
|
|
|
333
339
|
with _session_agents_lock:
|
|
334
340
|
state["stop_requested"] = True
|
|
335
341
|
state["last_update"] = time.time()
|
|
342
|
+
# Signal the stop event for immediate interruption
|
|
343
|
+
if state.get("stop_event"):
|
|
344
|
+
state["stop_event"].set()
|
|
336
345
|
else:
|
|
337
346
|
with _agent_state_lock:
|
|
338
347
|
_agent_state["stop_requested"] = True
|
|
339
348
|
_agent_state["last_update"] = time.time()
|
|
349
|
+
# Signal the stop event for immediate interruption
|
|
350
|
+
if _agent_state.get("stop_event"):
|
|
351
|
+
_agent_state["stop_event"].set()
|
|
340
352
|
|
|
341
353
|
|
|
342
354
|
def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: str = None, session_id: Optional[str] = None):
|
|
@@ -367,6 +379,12 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
367
379
|
current_state["running"] = False
|
|
368
380
|
return
|
|
369
381
|
|
|
382
|
+
# Create a stop event for immediate interruption
|
|
383
|
+
stop_event = threading.Event()
|
|
384
|
+
with state_lock:
|
|
385
|
+
current_state["stop_event"] = stop_event
|
|
386
|
+
current_state["stop_requested"] = False # Reset stop flag
|
|
387
|
+
|
|
370
388
|
# Track tool calls by their ID for updating status
|
|
371
389
|
tool_call_map = {}
|
|
372
390
|
|
|
@@ -413,6 +431,17 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
413
431
|
# Resume from interrupt
|
|
414
432
|
from langgraph.types import Command
|
|
415
433
|
agent_input = Command(resume=resume_data)
|
|
434
|
+
|
|
435
|
+
# Rebuild tool_call_map from existing tool calls and mark pending ones as running
|
|
436
|
+
with state_lock:
|
|
437
|
+
for tc in current_state.get("tool_calls", []):
|
|
438
|
+
tc_id = tc.get("id")
|
|
439
|
+
if tc_id:
|
|
440
|
+
tool_call_map[tc_id] = tc
|
|
441
|
+
# Mark pending tool calls back to running since we're resuming
|
|
442
|
+
if tc.get("status") == "pending":
|
|
443
|
+
tc["status"] = "running"
|
|
444
|
+
current_state["last_update"] = time.time()
|
|
416
445
|
else:
|
|
417
446
|
# Inject workspace context into the message if available
|
|
418
447
|
if workspace_path:
|
|
@@ -423,14 +452,15 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
423
452
|
agent_input = {"messages": [{"role": "user", "content": message_with_context}]}
|
|
424
453
|
|
|
425
454
|
for update in current_agent.stream(agent_input, stream_mode="updates", config=stream_config):
|
|
426
|
-
# Check if stop was requested
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
current_state["response"] = current_state.get("response", "") + "\n\
|
|
455
|
+
# Check if stop was requested (via flag or event)
|
|
456
|
+
if stop_event.is_set() or current_state.get("stop_requested"):
|
|
457
|
+
with state_lock:
|
|
458
|
+
current_state["response"] = current_state.get("response", "") + "\n\n⏹️ Agent stopped by user."
|
|
430
459
|
current_state["running"] = False
|
|
431
460
|
current_state["stop_requested"] = False
|
|
461
|
+
current_state["stop_event"] = None
|
|
432
462
|
current_state["last_update"] = time.time()
|
|
433
|
-
|
|
463
|
+
return
|
|
434
464
|
|
|
435
465
|
# Check for interrupt
|
|
436
466
|
if isinstance(update, dict) and "__interrupt__" in update:
|
|
@@ -439,6 +469,10 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
439
469
|
with state_lock:
|
|
440
470
|
current_state["interrupt"] = interrupt_data
|
|
441
471
|
current_state["running"] = False # Pause until user responds
|
|
472
|
+
# Mark any "running" tool calls as "pending" since we're waiting for user approval
|
|
473
|
+
for tc in current_state["tool_calls"]:
|
|
474
|
+
if tc.get("status") == "running":
|
|
475
|
+
tc["status"] = "pending"
|
|
442
476
|
current_state["last_update"] = time.time()
|
|
443
477
|
return # Exit stream, wait for user to resume
|
|
444
478
|
|
|
@@ -452,14 +486,26 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
452
486
|
|
|
453
487
|
# Capture AIMessage tool_calls
|
|
454
488
|
if msg_type == 'AIMessage' and hasattr(last_msg, 'tool_calls') and last_msg.tool_calls:
|
|
455
|
-
new_tool_calls = []
|
|
456
|
-
for tc in last_msg.tool_calls:
|
|
457
|
-
serialized = _serialize_tool_call(tc)
|
|
458
|
-
tool_call_map[serialized["id"]] = serialized
|
|
459
|
-
new_tool_calls.append(serialized)
|
|
460
|
-
|
|
461
489
|
with state_lock:
|
|
462
|
-
|
|
490
|
+
# Get existing tool call IDs to avoid duplicates
|
|
491
|
+
existing_ids = {tc.get("id") for tc in current_state["tool_calls"]}
|
|
492
|
+
|
|
493
|
+
for tc in last_msg.tool_calls:
|
|
494
|
+
serialized = _serialize_tool_call(tc)
|
|
495
|
+
tc_id = serialized["id"]
|
|
496
|
+
|
|
497
|
+
# Only add if not already in the list (avoid duplicates on resume)
|
|
498
|
+
if tc_id not in existing_ids:
|
|
499
|
+
tool_call_map[tc_id] = serialized
|
|
500
|
+
current_state["tool_calls"].append(serialized)
|
|
501
|
+
existing_ids.add(tc_id)
|
|
502
|
+
else:
|
|
503
|
+
# Update the map to reference the existing tool call
|
|
504
|
+
for existing_tc in current_state["tool_calls"]:
|
|
505
|
+
if existing_tc.get("id") == tc_id:
|
|
506
|
+
tool_call_map[tc_id] = existing_tc
|
|
507
|
+
break
|
|
508
|
+
|
|
463
509
|
current_state["last_update"] = time.time()
|
|
464
510
|
|
|
465
511
|
elif msg_type == 'ToolMessage' and hasattr(last_msg, 'name'):
|
|
@@ -488,10 +534,16 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
488
534
|
content_lower.startswith("traceback")):
|
|
489
535
|
status = "error"
|
|
490
536
|
|
|
491
|
-
#
|
|
492
|
-
|
|
493
|
-
if
|
|
494
|
-
|
|
537
|
+
# display_inline now pushes rich content directly to queue
|
|
538
|
+
# and returns a simple confirmation message, so no special handling needed
|
|
539
|
+
if isinstance(content, str):
|
|
540
|
+
# Truncate result for display
|
|
541
|
+
result_display = content[:1000] + "..." if len(content) > 1000 else content
|
|
542
|
+
else:
|
|
543
|
+
# Convert other types to string and truncate
|
|
544
|
+
result_display = str(content)
|
|
545
|
+
if len(result_display) > 1000:
|
|
546
|
+
result_display = result_display[:1000] + "..."
|
|
495
547
|
|
|
496
548
|
_update_tool_call_result(tool_call_id, result_display, status)
|
|
497
549
|
|
|
@@ -696,6 +748,7 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
696
748
|
|
|
697
749
|
with state_lock:
|
|
698
750
|
current_state["running"] = False
|
|
751
|
+
current_state["stop_event"] = None # Clean up stop event
|
|
699
752
|
current_state["last_update"] = time.time()
|
|
700
753
|
|
|
701
754
|
|
|
@@ -899,6 +952,7 @@ def resume_agent_from_interrupt(decision: str, action: str = "approve", action_r
|
|
|
899
952
|
|
|
900
953
|
with state_lock:
|
|
901
954
|
current_state["running"] = False
|
|
955
|
+
current_state["stop_event"] = None # Clean up stop event
|
|
902
956
|
current_state["response"] = f"Action rejected{tool_info}: {reject_message}"
|
|
903
957
|
current_state["last_update"] = time.time()
|
|
904
958
|
|
|
@@ -955,9 +1009,34 @@ def get_agent_state(session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
|
955
1009
|
state["tool_calls"] = copy.deepcopy(current_state["tool_calls"])
|
|
956
1010
|
state["todos"] = copy.deepcopy(current_state["todos"])
|
|
957
1011
|
state["canvas"] = copy.deepcopy(current_state["canvas"])
|
|
1012
|
+
state["display_inline_items"] = copy.deepcopy(current_state.get("display_inline_items", []))
|
|
958
1013
|
return state
|
|
959
1014
|
|
|
960
1015
|
|
|
1016
|
+
def push_display_inline_item(item: Dict[str, Any], session_id: Optional[str] = None):
|
|
1017
|
+
"""Push a display_inline item to the agent state (thread-safe).
|
|
1018
|
+
|
|
1019
|
+
This is called by the display_inline tool to store rich content
|
|
1020
|
+
that bypasses LangGraph serialization.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
item: The display result dict with type, display_type, data, etc.
|
|
1024
|
+
session_id: Session ID for virtual FS mode, None for physical FS mode.
|
|
1025
|
+
"""
|
|
1026
|
+
if USE_VIRTUAL_FS and session_id:
|
|
1027
|
+
current_state = _get_session_state(session_id)
|
|
1028
|
+
state_lock = _session_agents_lock
|
|
1029
|
+
else:
|
|
1030
|
+
current_state = _agent_state
|
|
1031
|
+
state_lock = _agent_state_lock
|
|
1032
|
+
|
|
1033
|
+
with state_lock:
|
|
1034
|
+
if "display_inline_items" not in current_state:
|
|
1035
|
+
current_state["display_inline_items"] = []
|
|
1036
|
+
current_state["display_inline_items"].append(item)
|
|
1037
|
+
current_state["last_update"] = time.time()
|
|
1038
|
+
|
|
1039
|
+
|
|
961
1040
|
def reset_agent_state(session_id: Optional[str] = None):
|
|
962
1041
|
"""Reset agent state for a fresh session (thread-safe).
|
|
963
1042
|
|
|
@@ -979,8 +1058,11 @@ def reset_agent_state(session_id: Optional[str] = None):
|
|
|
979
1058
|
current_state["thinking"] = ""
|
|
980
1059
|
current_state["todos"] = []
|
|
981
1060
|
current_state["tool_calls"] = []
|
|
1061
|
+
current_state["display_inline_items"] = []
|
|
982
1062
|
current_state["response"] = ""
|
|
983
1063
|
current_state["error"] = None
|
|
1064
|
+
current_state["stop_event"] = None
|
|
1065
|
+
current_state["stop_requested"] = False
|
|
984
1066
|
current_state["interrupt"] = None
|
|
985
1067
|
current_state["start_time"] = None
|
|
986
1068
|
current_state["stop_requested"] = False
|
|
@@ -1095,8 +1177,10 @@ def display_initial_messages(history, theme, skip_render, session_initialized, s
|
|
|
1095
1177
|
for msg in history:
|
|
1096
1178
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1097
1179
|
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=False, response_time=msg_response_time))
|
|
1180
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1098
1181
|
# Render tool calls stored with this message
|
|
1099
1182
|
if msg.get("tool_calls"):
|
|
1183
|
+
# Show collapsed tool calls section first
|
|
1100
1184
|
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1101
1185
|
if tool_calls_block:
|
|
1102
1186
|
messages.append(tool_calls_block)
|
|
@@ -1105,6 +1189,18 @@ def display_initial_messages(history, theme, skip_render, session_initialized, s
|
|
|
1105
1189
|
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1106
1190
|
if todos_block:
|
|
1107
1191
|
messages.append(todos_block)
|
|
1192
|
+
# Extract and show thinking from tool calls
|
|
1193
|
+
if msg.get("tool_calls"):
|
|
1194
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1195
|
+
messages.extend(thinking_blocks)
|
|
1196
|
+
# Extract and show display_inline results prominently
|
|
1197
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1198
|
+
messages.extend(inline_results)
|
|
1199
|
+
# Render display_inline items stored with this message
|
|
1200
|
+
if msg.get("display_inline_items"):
|
|
1201
|
+
for item in msg["display_inline_items"]:
|
|
1202
|
+
rendered = render_display_inline_result(item, colors)
|
|
1203
|
+
messages.append(rendered)
|
|
1108
1204
|
return messages, False, True, new_session_id
|
|
1109
1205
|
|
|
1110
1206
|
|
|
@@ -1138,7 +1234,7 @@ def initialize_file_tree_for_session(session_initialized, session_id, current_wo
|
|
|
1138
1234
|
current_workspace_dir = workspace_root.path(current_workspace) if current_workspace else workspace_root.root
|
|
1139
1235
|
|
|
1140
1236
|
# Build and render file tree
|
|
1141
|
-
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES)
|
|
1237
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, workspace_root=workspace_root)
|
|
1142
1238
|
|
|
1143
1239
|
|
|
1144
1240
|
# Chat callbacks
|
|
@@ -1173,8 +1269,9 @@ def handle_send_immediate(n_clicks, n_submit, message, history, theme, current_w
|
|
|
1173
1269
|
is_new = (i == len(history) - 1)
|
|
1174
1270
|
msg_response_time = m.get("response_time") if m["role"] == "assistant" else None
|
|
1175
1271
|
messages.append(format_message(m["role"], m["content"], colors, STYLES, is_new=is_new, response_time=msg_response_time))
|
|
1176
|
-
#
|
|
1272
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1177
1273
|
if m.get("tool_calls"):
|
|
1274
|
+
# Show collapsed tool calls section first
|
|
1178
1275
|
tool_calls_block = format_tool_calls_inline(m["tool_calls"], colors)
|
|
1179
1276
|
if tool_calls_block:
|
|
1180
1277
|
messages.append(tool_calls_block)
|
|
@@ -1183,6 +1280,13 @@ def handle_send_immediate(n_clicks, n_submit, message, history, theme, current_w
|
|
|
1183
1280
|
todos_block = format_todos_inline(m["todos"], colors)
|
|
1184
1281
|
if todos_block:
|
|
1185
1282
|
messages.append(todos_block)
|
|
1283
|
+
# Extract and show thinking from tool calls
|
|
1284
|
+
if m.get("tool_calls"):
|
|
1285
|
+
thinking_blocks = extract_thinking_from_tool_calls(m["tool_calls"], colors)
|
|
1286
|
+
messages.extend(thinking_blocks)
|
|
1287
|
+
# Extract and show display_inline results prominently
|
|
1288
|
+
inline_results = extract_display_inline_results(m["tool_calls"], colors)
|
|
1289
|
+
messages.extend(inline_results)
|
|
1186
1290
|
|
|
1187
1291
|
messages.append(format_loading(colors))
|
|
1188
1292
|
|
|
@@ -1228,14 +1332,18 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1228
1332
|
history = history or []
|
|
1229
1333
|
colors = get_colors(theme or "light")
|
|
1230
1334
|
|
|
1335
|
+
# Get display_inline items from agent state (bypasses LangGraph serialization)
|
|
1336
|
+
display_inline_items = state.get("display_inline_items", [])
|
|
1337
|
+
|
|
1231
1338
|
def render_history_messages(history_items):
|
|
1232
|
-
"""Render all history items including tool calls and todos."""
|
|
1339
|
+
"""Render all history items including tool calls, display_inline items, and todos."""
|
|
1233
1340
|
messages = []
|
|
1234
1341
|
for msg in history_items:
|
|
1235
1342
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1236
1343
|
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, response_time=msg_response_time))
|
|
1237
|
-
#
|
|
1344
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1238
1345
|
if msg.get("tool_calls"):
|
|
1346
|
+
# Show collapsed tool calls section first
|
|
1239
1347
|
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1240
1348
|
if tool_calls_block:
|
|
1241
1349
|
messages.append(tool_calls_block)
|
|
@@ -1244,6 +1352,18 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1244
1352
|
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1245
1353
|
if todos_block:
|
|
1246
1354
|
messages.append(todos_block)
|
|
1355
|
+
# Extract and show thinking from tool calls
|
|
1356
|
+
if msg.get("tool_calls"):
|
|
1357
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1358
|
+
messages.extend(thinking_blocks)
|
|
1359
|
+
# Extract and show display_inline results prominently
|
|
1360
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1361
|
+
messages.extend(inline_results)
|
|
1362
|
+
# Render display_inline items stored with this message
|
|
1363
|
+
if msg.get("display_inline_items"):
|
|
1364
|
+
for item in msg["display_inline_items"]:
|
|
1365
|
+
rendered = render_display_inline_result(item, colors)
|
|
1366
|
+
messages.append(rendered)
|
|
1247
1367
|
return messages
|
|
1248
1368
|
|
|
1249
1369
|
# Check for interrupt (human-in-the-loop)
|
|
@@ -1251,13 +1371,9 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1251
1371
|
# Agent is paused waiting for user input
|
|
1252
1372
|
messages = render_history_messages(history)
|
|
1253
1373
|
|
|
1254
|
-
#
|
|
1255
|
-
if state["thinking"]:
|
|
1256
|
-
thinking_block = format_thinking(state["thinking"], colors)
|
|
1257
|
-
if thinking_block:
|
|
1258
|
-
messages.append(thinking_block)
|
|
1259
|
-
|
|
1374
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1260
1375
|
if state.get("tool_calls"):
|
|
1376
|
+
# Show collapsed tool calls section first
|
|
1261
1377
|
tool_calls_block = format_tool_calls_inline(state["tool_calls"], colors)
|
|
1262
1378
|
if tool_calls_block:
|
|
1263
1379
|
messages.append(tool_calls_block)
|
|
@@ -1267,6 +1383,19 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1267
1383
|
if todos_block:
|
|
1268
1384
|
messages.append(todos_block)
|
|
1269
1385
|
|
|
1386
|
+
if state.get("tool_calls"):
|
|
1387
|
+
# Extract and show thinking from tool calls
|
|
1388
|
+
thinking_blocks = extract_thinking_from_tool_calls(state["tool_calls"], colors)
|
|
1389
|
+
messages.extend(thinking_blocks)
|
|
1390
|
+
# Extract and show display_inline results prominently
|
|
1391
|
+
inline_results = extract_display_inline_results(state["tool_calls"], colors)
|
|
1392
|
+
messages.extend(inline_results)
|
|
1393
|
+
|
|
1394
|
+
# Render any queued display_inline items (bypasses LangGraph serialization)
|
|
1395
|
+
for item in display_inline_items:
|
|
1396
|
+
rendered = render_display_inline_result(item, colors)
|
|
1397
|
+
messages.append(rendered)
|
|
1398
|
+
|
|
1270
1399
|
# Add interrupt UI
|
|
1271
1400
|
interrupt_block = format_interrupt(state["interrupt"], colors)
|
|
1272
1401
|
if interrupt_block:
|
|
@@ -1282,15 +1411,20 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1282
1411
|
if state.get("start_time"):
|
|
1283
1412
|
response_time = time.time() - state["start_time"]
|
|
1284
1413
|
|
|
1285
|
-
# Agent finished - store tool calls and
|
|
1414
|
+
# Agent finished - store tool calls, todos, and display_inline items with the USER message
|
|
1415
|
+
# (they appear after user msg in the UI)
|
|
1416
|
+
saved_display_inline_items = False
|
|
1286
1417
|
if history:
|
|
1287
|
-
# Find the last user message and attach tool calls and
|
|
1418
|
+
# Find the last user message and attach tool calls, todos, and display_inline items to it
|
|
1288
1419
|
for i in range(len(history) - 1, -1, -1):
|
|
1289
1420
|
if history[i]["role"] == "user":
|
|
1290
1421
|
if state.get("tool_calls"):
|
|
1291
1422
|
history[i]["tool_calls"] = state["tool_calls"]
|
|
1292
1423
|
if state.get("todos"):
|
|
1293
1424
|
history[i]["todos"] = state["todos"]
|
|
1425
|
+
if display_inline_items:
|
|
1426
|
+
history[i]["display_inline_items"] = display_inline_items
|
|
1427
|
+
saved_display_inline_items = True
|
|
1294
1428
|
break
|
|
1295
1429
|
|
|
1296
1430
|
# Add assistant response to history (with response time)
|
|
@@ -1303,12 +1437,13 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1303
1437
|
history.append(assistant_msg)
|
|
1304
1438
|
|
|
1305
1439
|
# Render all history (tool calls and todos are now part of history)
|
|
1440
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1306
1441
|
final_messages = []
|
|
1307
1442
|
for i, msg in enumerate(history):
|
|
1308
1443
|
is_new = (i >= len(history) - 1)
|
|
1309
1444
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1310
1445
|
final_messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=is_new, response_time=msg_response_time))
|
|
1311
|
-
#
|
|
1446
|
+
# Show collapsed tool calls section first
|
|
1312
1447
|
if msg.get("tool_calls"):
|
|
1313
1448
|
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1314
1449
|
if tool_calls_block:
|
|
@@ -1318,21 +1453,35 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1318
1453
|
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1319
1454
|
if todos_block:
|
|
1320
1455
|
final_messages.append(todos_block)
|
|
1456
|
+
# Extract and show thinking from tool calls
|
|
1457
|
+
if msg.get("tool_calls"):
|
|
1458
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1459
|
+
final_messages.extend(thinking_blocks)
|
|
1460
|
+
# Extract and show display_inline results prominently
|
|
1461
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1462
|
+
final_messages.extend(inline_results)
|
|
1463
|
+
# Render display_inline items stored with this message
|
|
1464
|
+
if msg.get("display_inline_items"):
|
|
1465
|
+
for item in msg["display_inline_items"]:
|
|
1466
|
+
rendered = render_display_inline_result(item, colors)
|
|
1467
|
+
final_messages.append(rendered)
|
|
1468
|
+
|
|
1469
|
+
# Render any NEW queued display_inline items only if not already saved to history
|
|
1470
|
+
# (avoids duplicate rendering)
|
|
1471
|
+
if not saved_display_inline_items:
|
|
1472
|
+
for item in display_inline_items:
|
|
1473
|
+
rendered = render_display_inline_result(item, colors)
|
|
1474
|
+
final_messages.append(rendered)
|
|
1321
1475
|
|
|
1322
1476
|
# Disable polling, set skip flag to prevent display_initial_messages from re-rendering
|
|
1323
1477
|
return final_messages, history, True, True
|
|
1324
1478
|
else:
|
|
1325
|
-
# Agent still running - show loading with current
|
|
1479
|
+
# Agent still running - show loading with current tool_calls/todos/thinking
|
|
1326
1480
|
messages = render_history_messages(history)
|
|
1327
1481
|
|
|
1328
|
-
#
|
|
1329
|
-
if state["thinking"]:
|
|
1330
|
-
thinking_block = format_thinking(state["thinking"], colors)
|
|
1331
|
-
if thinking_block:
|
|
1332
|
-
messages.append(thinking_block)
|
|
1333
|
-
|
|
1334
|
-
# Add current tool calls if available
|
|
1482
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1335
1483
|
if state.get("tool_calls"):
|
|
1484
|
+
# Show collapsed tool calls section first
|
|
1336
1485
|
tool_calls_block = format_tool_calls_inline(state["tool_calls"], colors)
|
|
1337
1486
|
if tool_calls_block:
|
|
1338
1487
|
messages.append(tool_calls_block)
|
|
@@ -1343,6 +1492,19 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1343
1492
|
if todos_block:
|
|
1344
1493
|
messages.append(todos_block)
|
|
1345
1494
|
|
|
1495
|
+
if state.get("tool_calls"):
|
|
1496
|
+
# Extract and show thinking from tool calls
|
|
1497
|
+
thinking_blocks = extract_thinking_from_tool_calls(state["tool_calls"], colors)
|
|
1498
|
+
messages.extend(thinking_blocks)
|
|
1499
|
+
# Extract and show display_inline results prominently
|
|
1500
|
+
inline_results = extract_display_inline_results(state["tool_calls"], colors)
|
|
1501
|
+
messages.extend(inline_results)
|
|
1502
|
+
|
|
1503
|
+
# Render any queued display_inline items (bypasses LangGraph serialization)
|
|
1504
|
+
for item in display_inline_items:
|
|
1505
|
+
rendered = render_display_inline_result(item, colors)
|
|
1506
|
+
messages.append(rendered)
|
|
1507
|
+
|
|
1346
1508
|
# Add loading indicator
|
|
1347
1509
|
messages.append(format_loading(colors))
|
|
1348
1510
|
|
|
@@ -1388,12 +1550,14 @@ def handle_stop_button(n_clicks, history, theme, session_id):
|
|
|
1388
1550
|
request_agent_stop(session_id)
|
|
1389
1551
|
|
|
1390
1552
|
# Render current messages with a stopping indicator
|
|
1553
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1391
1554
|
def render_history_messages(history):
|
|
1392
1555
|
messages = []
|
|
1393
1556
|
for i, msg in enumerate(history):
|
|
1394
1557
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1395
1558
|
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=False, response_time=msg_response_time))
|
|
1396
1559
|
if msg.get("tool_calls"):
|
|
1560
|
+
# Show collapsed tool calls section first
|
|
1397
1561
|
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1398
1562
|
if tool_calls_block:
|
|
1399
1563
|
messages.append(tool_calls_block)
|
|
@@ -1401,6 +1565,18 @@ def handle_stop_button(n_clicks, history, theme, session_id):
|
|
|
1401
1565
|
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1402
1566
|
if todos_block:
|
|
1403
1567
|
messages.append(todos_block)
|
|
1568
|
+
if msg.get("tool_calls"):
|
|
1569
|
+
# Extract and show thinking from tool calls
|
|
1570
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1571
|
+
messages.extend(thinking_blocks)
|
|
1572
|
+
# Extract and show display_inline results prominently
|
|
1573
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1574
|
+
messages.extend(inline_results)
|
|
1575
|
+
# Render display_inline items stored with this message
|
|
1576
|
+
if msg.get("display_inline_items"):
|
|
1577
|
+
for item in msg["display_inline_items"]:
|
|
1578
|
+
rendered = render_display_inline_result(item, colors)
|
|
1579
|
+
messages.append(rendered)
|
|
1404
1580
|
return messages
|
|
1405
1581
|
|
|
1406
1582
|
messages = render_history_messages(history)
|
|
@@ -1476,12 +1652,13 @@ def handle_interrupt_response(approve_clicks, reject_clicks, edit_clicks, input_
|
|
|
1476
1652
|
resume_agent_from_interrupt(decision, action, session_id=session_id)
|
|
1477
1653
|
|
|
1478
1654
|
# Show loading state while agent resumes
|
|
1655
|
+
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1479
1656
|
messages = []
|
|
1480
1657
|
for msg in history:
|
|
1481
1658
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1482
1659
|
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, response_time=msg_response_time))
|
|
1483
|
-
# Render tool calls stored with this message
|
|
1484
1660
|
if msg.get("tool_calls"):
|
|
1661
|
+
# Show collapsed tool calls section first
|
|
1485
1662
|
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1486
1663
|
if tool_calls_block:
|
|
1487
1664
|
messages.append(tool_calls_block)
|
|
@@ -1490,6 +1667,18 @@ def handle_interrupt_response(approve_clicks, reject_clicks, edit_clicks, input_
|
|
|
1490
1667
|
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1491
1668
|
if todos_block:
|
|
1492
1669
|
messages.append(todos_block)
|
|
1670
|
+
if msg.get("tool_calls"):
|
|
1671
|
+
# Extract and show thinking from tool calls
|
|
1672
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1673
|
+
messages.extend(thinking_blocks)
|
|
1674
|
+
# Extract and show display_inline results prominently
|
|
1675
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1676
|
+
messages.extend(inline_results)
|
|
1677
|
+
# Render display_inline items stored with this message
|
|
1678
|
+
if msg.get("display_inline_items"):
|
|
1679
|
+
for item in msg["display_inline_items"]:
|
|
1680
|
+
rendered = render_display_inline_result(item, colors)
|
|
1681
|
+
messages.append(rendered)
|
|
1493
1682
|
|
|
1494
1683
|
messages.append(format_loading(colors))
|
|
1495
1684
|
|
|
@@ -1576,7 +1765,8 @@ def toggle_folder(n_clicks, header_ids, real_paths, children_ids, icon_ids, chil
|
|
|
1576
1765
|
loaded_content = render_file_tree(folder_items, colors, STYLES,
|
|
1577
1766
|
level=folder_rel_path.count("/") + folder_rel_path.count("\\") + 1,
|
|
1578
1767
|
parent_path=folder_rel_path,
|
|
1579
|
-
expanded_folders=expanded_folders
|
|
1768
|
+
expanded_folders=expanded_folders,
|
|
1769
|
+
workspace_root=workspace_root)
|
|
1580
1770
|
new_children_content.append(loaded_content if loaded_content else current_content)
|
|
1581
1771
|
except Exception as e:
|
|
1582
1772
|
print(f"Error loading folder {folder_rel_path}: {e}")
|
|
@@ -1748,7 +1938,8 @@ def enter_folder(folder_clicks, root_clicks, breadcrumb_clicks, folder_ids, fold
|
|
|
1748
1938
|
# Render new file tree (reset expanded folders when navigating)
|
|
1749
1939
|
file_tree = render_file_tree(
|
|
1750
1940
|
build_file_tree(workspace_full_path, workspace_full_path),
|
|
1751
|
-
colors, STYLES
|
|
1941
|
+
colors, STYLES,
|
|
1942
|
+
workspace_root=workspace_root
|
|
1752
1943
|
)
|
|
1753
1944
|
|
|
1754
1945
|
return new_path, breadcrumb_children, file_tree, [] # Reset expanded folders
|
|
@@ -2014,6 +2205,141 @@ def open_file_modal(all_n_clicks, all_ids, click_tracker, theme, session_id):
|
|
|
2014
2205
|
"color": colors["text_primary"],
|
|
2015
2206
|
}
|
|
2016
2207
|
)
|
|
2208
|
+
elif file_ext in ('.csv', '.tsv'):
|
|
2209
|
+
# CSV/TSV files - render as table with raw view option
|
|
2210
|
+
import io as _io
|
|
2211
|
+
try:
|
|
2212
|
+
import pandas as pd
|
|
2213
|
+
sep = '\t' if file_ext == '.tsv' else ','
|
|
2214
|
+
df = pd.read_csv(_io.StringIO(content), sep=sep)
|
|
2215
|
+
|
|
2216
|
+
# Pagination settings
|
|
2217
|
+
rows_per_page = 50
|
|
2218
|
+
total_rows = len(df)
|
|
2219
|
+
total_pages = max(1, (total_rows + rows_per_page - 1) // rows_per_page)
|
|
2220
|
+
current_page = 0
|
|
2221
|
+
|
|
2222
|
+
# Create table preview (first page)
|
|
2223
|
+
start_idx = current_page * rows_per_page
|
|
2224
|
+
end_idx = min(start_idx + rows_per_page, total_rows)
|
|
2225
|
+
preview_df = df.iloc[start_idx:end_idx]
|
|
2226
|
+
|
|
2227
|
+
# Row info for display
|
|
2228
|
+
if total_rows > rows_per_page:
|
|
2229
|
+
row_info = f"Rows {start_idx + 1}-{end_idx} of {total_rows}"
|
|
2230
|
+
else:
|
|
2231
|
+
row_info = f"{total_rows} rows"
|
|
2232
|
+
|
|
2233
|
+
modal_content = html.Div([
|
|
2234
|
+
# Tab buttons for switching views
|
|
2235
|
+
html.Div([
|
|
2236
|
+
html.Button("Table", id="html-preview-tab", n_clicks=0,
|
|
2237
|
+
className="html-tab-btn html-tab-active",
|
|
2238
|
+
style={"marginRight": "8px", "padding": "6px 12px", "border": "none",
|
|
2239
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
2240
|
+
"background": colors["accent"], "color": "#fff"}),
|
|
2241
|
+
html.Button("Raw", id="html-source-tab", n_clicks=0,
|
|
2242
|
+
className="html-tab-btn",
|
|
2243
|
+
style={"padding": "6px 12px", "border": f"1px solid {colors['border']}",
|
|
2244
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
2245
|
+
"background": "transparent", "color": colors["text_primary"]}),
|
|
2246
|
+
], style={"marginBottom": "12px", "display": "flex"}),
|
|
2247
|
+
# Row count info and pagination controls
|
|
2248
|
+
html.Div([
|
|
2249
|
+
html.Span(f"{len(df.columns)} columns, {row_info}", id="csv-row-info", style={
|
|
2250
|
+
"fontSize": "12px",
|
|
2251
|
+
"color": colors["text_muted"],
|
|
2252
|
+
}),
|
|
2253
|
+
# Pagination controls (only show if more than one page)
|
|
2254
|
+
html.Div([
|
|
2255
|
+
html.Button("◀", id="csv-prev-page", n_clicks=0,
|
|
2256
|
+
disabled=current_page == 0,
|
|
2257
|
+
style={
|
|
2258
|
+
"padding": "4px 8px", "border": f"1px solid {colors['border']}",
|
|
2259
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
2260
|
+
"background": "transparent", "color": colors["text_primary"],
|
|
2261
|
+
"marginRight": "8px", "fontSize": "12px",
|
|
2262
|
+
}),
|
|
2263
|
+
html.Span(f"Page {current_page + 1} of {total_pages}", id="csv-page-info",
|
|
2264
|
+
style={"fontSize": "12px", "color": colors["text_primary"]}),
|
|
2265
|
+
html.Button("▶", id="csv-next-page", n_clicks=0,
|
|
2266
|
+
disabled=current_page >= total_pages - 1,
|
|
2267
|
+
style={
|
|
2268
|
+
"padding": "4px 8px", "border": f"1px solid {colors['border']}",
|
|
2269
|
+
"borderRadius": "4px", "cursor": "pointer",
|
|
2270
|
+
"background": "transparent", "color": colors["text_primary"],
|
|
2271
|
+
"marginLeft": "8px", "fontSize": "12px",
|
|
2272
|
+
}),
|
|
2273
|
+
], style={"display": "flex" if total_pages > 1 else "none", "alignItems": "center"}),
|
|
2274
|
+
], style={"display": "flex", "justifyContent": "space-between", "alignItems": "center", "marginBottom": "8px"}),
|
|
2275
|
+
# Store CSV data for pagination
|
|
2276
|
+
dcc.Store(id="csv-data-store", data={
|
|
2277
|
+
"content": content,
|
|
2278
|
+
"sep": sep,
|
|
2279
|
+
"total_rows": total_rows,
|
|
2280
|
+
"total_pages": total_pages,
|
|
2281
|
+
"rows_per_page": rows_per_page,
|
|
2282
|
+
"current_page": current_page,
|
|
2283
|
+
}),
|
|
2284
|
+
# Table preview (default visible)
|
|
2285
|
+
html.Div([
|
|
2286
|
+
dcc.Markdown(
|
|
2287
|
+
preview_df.to_html(index=False, classes="csv-preview-table"),
|
|
2288
|
+
dangerously_allow_html=True,
|
|
2289
|
+
style={"overflow": "auto"}
|
|
2290
|
+
)
|
|
2291
|
+
], id="html-preview-frame", className="csv-table-container", style={
|
|
2292
|
+
"border": f"1px solid {colors['border']}",
|
|
2293
|
+
"borderRadius": "4px",
|
|
2294
|
+
"background": colors["bg_secondary"],
|
|
2295
|
+
"maxHeight": "65vh",
|
|
2296
|
+
"overflow": "auto",
|
|
2297
|
+
}),
|
|
2298
|
+
# Raw CSV (hidden by default)
|
|
2299
|
+
html.Pre(
|
|
2300
|
+
content,
|
|
2301
|
+
id="html-source-code",
|
|
2302
|
+
style={
|
|
2303
|
+
"display": "none",
|
|
2304
|
+
"background": colors["bg_tertiary"],
|
|
2305
|
+
"padding": "16px",
|
|
2306
|
+
"fontSize": "12px",
|
|
2307
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
2308
|
+
"overflow": "auto",
|
|
2309
|
+
"maxHeight": "80vh",
|
|
2310
|
+
"whiteSpace": "pre-wrap",
|
|
2311
|
+
"wordBreak": "break-word",
|
|
2312
|
+
"margin": "0",
|
|
2313
|
+
"color": colors["text_primary"],
|
|
2314
|
+
"border": f"1px solid {colors['border']}",
|
|
2315
|
+
"borderRadius": "4px",
|
|
2316
|
+
}
|
|
2317
|
+
)
|
|
2318
|
+
])
|
|
2319
|
+
except Exception as e:
|
|
2320
|
+
# Fall back to raw text if parsing fails
|
|
2321
|
+
modal_content = html.Div([
|
|
2322
|
+
html.Div(f"Could not parse as CSV: {e}", style={
|
|
2323
|
+
"fontSize": "12px",
|
|
2324
|
+
"color": colors["text_muted"],
|
|
2325
|
+
"marginBottom": "8px",
|
|
2326
|
+
}),
|
|
2327
|
+
html.Pre(
|
|
2328
|
+
content,
|
|
2329
|
+
style={
|
|
2330
|
+
"background": colors["bg_tertiary"],
|
|
2331
|
+
"padding": "16px",
|
|
2332
|
+
"fontSize": "12px",
|
|
2333
|
+
"fontFamily": "'IBM Plex Mono', monospace",
|
|
2334
|
+
"overflow": "auto",
|
|
2335
|
+
"maxHeight": "80vh",
|
|
2336
|
+
"whiteSpace": "pre-wrap",
|
|
2337
|
+
"wordBreak": "break-word",
|
|
2338
|
+
"margin": "0",
|
|
2339
|
+
"color": colors["text_primary"],
|
|
2340
|
+
}
|
|
2341
|
+
)
|
|
2342
|
+
])
|
|
2017
2343
|
else:
|
|
2018
2344
|
# Regular text files
|
|
2019
2345
|
modal_content = html.Pre(
|
|
@@ -2087,10 +2413,12 @@ def download_from_modal(n_clicks, file_path, session_id):
|
|
|
2087
2413
|
Output("html-source-tab", "style")],
|
|
2088
2414
|
[Input("html-preview-tab", "n_clicks"),
|
|
2089
2415
|
Input("html-source-tab", "n_clicks")],
|
|
2090
|
-
[State("theme-store", "data")
|
|
2416
|
+
[State("theme-store", "data"),
|
|
2417
|
+
State("html-preview-frame", "style"),
|
|
2418
|
+
State("html-source-code", "style")],
|
|
2091
2419
|
prevent_initial_call=True
|
|
2092
2420
|
)
|
|
2093
|
-
def toggle_html_view(preview_clicks, source_clicks, theme):
|
|
2421
|
+
def toggle_html_view(preview_clicks, source_clicks, theme, current_preview_style, current_source_style):
|
|
2094
2422
|
"""Toggle between HTML preview and source code view."""
|
|
2095
2423
|
ctx = callback_context
|
|
2096
2424
|
if not ctx.triggered:
|
|
@@ -2099,28 +2427,18 @@ def toggle_html_view(preview_clicks, source_clicks, theme):
|
|
|
2099
2427
|
colors = get_colors(theme or "light")
|
|
2100
2428
|
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
2101
2429
|
|
|
2102
|
-
#
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
}
|
|
2110
|
-
source_code_style = {
|
|
2430
|
+
# Preserve current styles and only update display property
|
|
2431
|
+
# This ensures background colors set by the modal content are preserved
|
|
2432
|
+
preview_frame_style = current_preview_style.copy() if current_preview_style else {}
|
|
2433
|
+
source_code_style = current_source_style.copy() if current_source_style else {}
|
|
2434
|
+
|
|
2435
|
+
# Update theme-sensitive properties
|
|
2436
|
+
source_code_style.update({
|
|
2111
2437
|
"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
2438
|
"color": colors["text_primary"],
|
|
2121
2439
|
"border": f"1px solid {colors['border']}",
|
|
2122
|
-
|
|
2123
|
-
|
|
2440
|
+
})
|
|
2441
|
+
|
|
2124
2442
|
active_btn_style = {
|
|
2125
2443
|
"marginRight": "8px", "padding": "6px 12px", "border": "none",
|
|
2126
2444
|
"borderRadius": "4px", "cursor": "pointer",
|
|
@@ -2144,6 +2462,85 @@ def toggle_html_view(preview_clicks, source_clicks, theme):
|
|
|
2144
2462
|
return preview_frame_style, source_code_style, active_btn_style, {**inactive_btn_style}
|
|
2145
2463
|
|
|
2146
2464
|
|
|
2465
|
+
# CSV pagination
|
|
2466
|
+
@app.callback(
|
|
2467
|
+
[Output("html-preview-frame", "children", allow_duplicate=True),
|
|
2468
|
+
Output("csv-row-info", "children"),
|
|
2469
|
+
Output("csv-page-info", "children"),
|
|
2470
|
+
Output("csv-prev-page", "disabled"),
|
|
2471
|
+
Output("csv-next-page", "disabled"),
|
|
2472
|
+
Output("csv-data-store", "data")],
|
|
2473
|
+
[Input("csv-prev-page", "n_clicks"),
|
|
2474
|
+
Input("csv-next-page", "n_clicks")],
|
|
2475
|
+
[State("csv-data-store", "data"),
|
|
2476
|
+
State("theme-store", "data")],
|
|
2477
|
+
prevent_initial_call=True
|
|
2478
|
+
)
|
|
2479
|
+
def paginate_csv(prev_clicks, next_clicks, csv_data, theme):
|
|
2480
|
+
"""Handle CSV pagination."""
|
|
2481
|
+
ctx = callback_context
|
|
2482
|
+
if not ctx.triggered or not csv_data:
|
|
2483
|
+
raise PreventUpdate
|
|
2484
|
+
|
|
2485
|
+
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
2486
|
+
|
|
2487
|
+
import io as _io
|
|
2488
|
+
import pandas as pd
|
|
2489
|
+
|
|
2490
|
+
# Get current state
|
|
2491
|
+
content = csv_data.get("content", "")
|
|
2492
|
+
sep = csv_data.get("sep", ",")
|
|
2493
|
+
total_rows = csv_data.get("total_rows", 0)
|
|
2494
|
+
total_pages = csv_data.get("total_pages", 1)
|
|
2495
|
+
rows_per_page = csv_data.get("rows_per_page", 50)
|
|
2496
|
+
current_page = csv_data.get("current_page", 0)
|
|
2497
|
+
|
|
2498
|
+
# Update page based on which button was clicked
|
|
2499
|
+
if triggered_id == "csv-prev-page" and current_page > 0:
|
|
2500
|
+
current_page -= 1
|
|
2501
|
+
elif triggered_id == "csv-next-page" and current_page < total_pages - 1:
|
|
2502
|
+
current_page += 1
|
|
2503
|
+
else:
|
|
2504
|
+
raise PreventUpdate
|
|
2505
|
+
|
|
2506
|
+
# Parse CSV and get the page slice
|
|
2507
|
+
try:
|
|
2508
|
+
df = pd.read_csv(_io.StringIO(content), sep=sep)
|
|
2509
|
+
start_idx = current_page * rows_per_page
|
|
2510
|
+
end_idx = min(start_idx + rows_per_page, total_rows)
|
|
2511
|
+
preview_df = df.iloc[start_idx:end_idx]
|
|
2512
|
+
|
|
2513
|
+
# Generate row info
|
|
2514
|
+
if total_rows > rows_per_page:
|
|
2515
|
+
row_info = f"{len(df.columns)} columns, Rows {start_idx + 1}-{end_idx} of {total_rows}"
|
|
2516
|
+
else:
|
|
2517
|
+
row_info = f"{len(df.columns)} columns, {total_rows} rows"
|
|
2518
|
+
|
|
2519
|
+
# Generate table HTML
|
|
2520
|
+
table_html = dcc.Markdown(
|
|
2521
|
+
preview_df.to_html(index=False, classes="csv-preview-table"),
|
|
2522
|
+
dangerously_allow_html=True,
|
|
2523
|
+
style={"overflow": "auto"}
|
|
2524
|
+
)
|
|
2525
|
+
|
|
2526
|
+
# Update pagination state
|
|
2527
|
+
updated_csv_data = {
|
|
2528
|
+
**csv_data,
|
|
2529
|
+
"current_page": current_page,
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
return (
|
|
2533
|
+
table_html,
|
|
2534
|
+
row_info,
|
|
2535
|
+
f"Page {current_page + 1} of {total_pages}",
|
|
2536
|
+
current_page == 0, # prev disabled
|
|
2537
|
+
current_page >= total_pages - 1, # next disabled
|
|
2538
|
+
updated_csv_data
|
|
2539
|
+
)
|
|
2540
|
+
except Exception:
|
|
2541
|
+
raise PreventUpdate
|
|
2542
|
+
|
|
2543
|
+
|
|
2147
2544
|
# Open terminal
|
|
2148
2545
|
@app.callback(
|
|
2149
2546
|
Output("open-terminal-btn", "n_clicks"),
|
|
@@ -2216,7 +2613,7 @@ def refresh_sidebar(n_clicks, current_workspace, theme, collapsed_ids, session_i
|
|
|
2216
2613
|
current_workspace_dir = workspace_root / current_workspace if current_workspace else workspace_root
|
|
2217
2614
|
|
|
2218
2615
|
# 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)
|
|
2616
|
+
file_tree = render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders, workspace_root=workspace_root)
|
|
2220
2617
|
|
|
2221
2618
|
# Re-render canvas from current in-memory state (don't reload from file)
|
|
2222
2619
|
# This preserves canvas items that may not have been exported to .canvas/canvas.md yet
|
|
@@ -2269,7 +2666,7 @@ def handle_sidebar_upload(contents, filenames, current_workspace, theme, session
|
|
|
2269
2666
|
except Exception as e:
|
|
2270
2667
|
print(f"Upload error: {e}")
|
|
2271
2668
|
|
|
2272
|
-
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders)
|
|
2669
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders, workspace_root=workspace_root)
|
|
2273
2670
|
|
|
2274
2671
|
|
|
2275
2672
|
# Create folder modal - open
|
|
@@ -2350,7 +2747,7 @@ def create_folder(n_clicks, folder_name, current_workspace, theme, session_id, e
|
|
|
2350
2747
|
|
|
2351
2748
|
try:
|
|
2352
2749
|
folder_path.mkdir(parents=True, exist_ok=False)
|
|
2353
|
-
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders), "", ""
|
|
2750
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders, workspace_root=workspace_root), "", ""
|
|
2354
2751
|
except Exception as e:
|
|
2355
2752
|
return no_update, f"Error creating folder: {e}", no_update
|
|
2356
2753
|
|
|
@@ -2474,7 +2871,7 @@ def poll_file_tree_update(n_intervals, current_workspace, theme, session_id, vie
|
|
|
2474
2871
|
current_workspace_dir = workspace_root / current_workspace if current_workspace else workspace_root
|
|
2475
2872
|
|
|
2476
2873
|
# 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)
|
|
2874
|
+
return render_file_tree(build_file_tree(current_workspace_dir, current_workspace_dir), colors, STYLES, expanded_folders=expanded_folders, workspace_root=workspace_root)
|
|
2478
2875
|
|
|
2479
2876
|
|
|
2480
2877
|
# Open clear canvas confirmation modal
|
|
@@ -2760,6 +3157,133 @@ def handle_delete_confirmation(confirm_clicks, cancel_clicks, item_id, theme, co
|
|
|
2760
3157
|
raise PreventUpdate
|
|
2761
3158
|
|
|
2762
3159
|
|
|
3160
|
+
# =============================================================================
|
|
3161
|
+
# ADD DISPLAY_INLINE TO CANVAS CALLBACK
|
|
3162
|
+
# =============================================================================
|
|
3163
|
+
|
|
3164
|
+
@app.callback(
|
|
3165
|
+
[Output("canvas-content", "children", allow_duplicate=True),
|
|
3166
|
+
Output("sidebar-view-toggle", "value", allow_duplicate=True)],
|
|
3167
|
+
Input({"type": "add-display-to-canvas-btn", "index": ALL}, "n_clicks"),
|
|
3168
|
+
[State({"type": "display-inline-data", "index": ALL}, "data"),
|
|
3169
|
+
State("theme-store", "data"),
|
|
3170
|
+
State("collapsed-canvas-items", "data"),
|
|
3171
|
+
State("session-id", "data")],
|
|
3172
|
+
prevent_initial_call=True
|
|
3173
|
+
)
|
|
3174
|
+
def add_display_inline_to_canvas(n_clicks_list, data_list, theme, collapsed_ids, session_id):
|
|
3175
|
+
"""Add a display_inline item to the canvas when the button is clicked.
|
|
3176
|
+
|
|
3177
|
+
This allows users to save inline display items to the canvas for persistent reference.
|
|
3178
|
+
"""
|
|
3179
|
+
from .canvas import generate_canvas_id, export_canvas_to_markdown
|
|
3180
|
+
from datetime import datetime
|
|
3181
|
+
|
|
3182
|
+
# Check if any button was actually clicked
|
|
3183
|
+
if not n_clicks_list or not any(n_clicks_list):
|
|
3184
|
+
raise PreventUpdate
|
|
3185
|
+
|
|
3186
|
+
# Find which button was clicked
|
|
3187
|
+
ctx = callback_context
|
|
3188
|
+
if not ctx.triggered:
|
|
3189
|
+
raise PreventUpdate
|
|
3190
|
+
|
|
3191
|
+
triggered = ctx.triggered[0]
|
|
3192
|
+
triggered_id = triggered["prop_id"]
|
|
3193
|
+
|
|
3194
|
+
# Parse the pattern-matching ID to get the index
|
|
3195
|
+
try:
|
|
3196
|
+
# Format: {"type":"add-display-to-canvas-btn","index":"abc123"}.n_clicks
|
|
3197
|
+
id_part = triggered_id.rsplit(".", 1)[0]
|
|
3198
|
+
id_dict = json.loads(id_part)
|
|
3199
|
+
clicked_index = id_dict.get("index")
|
|
3200
|
+
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
3201
|
+
raise PreventUpdate
|
|
3202
|
+
|
|
3203
|
+
if not clicked_index:
|
|
3204
|
+
raise PreventUpdate
|
|
3205
|
+
|
|
3206
|
+
# Find the corresponding data
|
|
3207
|
+
display_data = None
|
|
3208
|
+
for data in data_list:
|
|
3209
|
+
if data and data.get("_item_id") == clicked_index:
|
|
3210
|
+
display_data = data
|
|
3211
|
+
break
|
|
3212
|
+
|
|
3213
|
+
if not display_data:
|
|
3214
|
+
raise PreventUpdate
|
|
3215
|
+
|
|
3216
|
+
colors = get_colors(theme or "light")
|
|
3217
|
+
collapsed_ids = collapsed_ids or []
|
|
3218
|
+
|
|
3219
|
+
# Get workspace for this session (virtual or physical)
|
|
3220
|
+
workspace_root = get_workspace_for_session(session_id)
|
|
3221
|
+
|
|
3222
|
+
# Convert display_inline result to canvas item format
|
|
3223
|
+
display_type = display_data.get("display_type", "text")
|
|
3224
|
+
title = display_data.get("title")
|
|
3225
|
+
data = display_data.get("data")
|
|
3226
|
+
|
|
3227
|
+
# Generate new canvas ID and timestamp
|
|
3228
|
+
canvas_id = generate_canvas_id()
|
|
3229
|
+
created_at = datetime.now().isoformat()
|
|
3230
|
+
|
|
3231
|
+
# Map display_inline types to canvas types
|
|
3232
|
+
canvas_item = {
|
|
3233
|
+
"id": canvas_id,
|
|
3234
|
+
"created_at": created_at,
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
if title:
|
|
3238
|
+
canvas_item["title"] = title
|
|
3239
|
+
|
|
3240
|
+
if display_type == "image":
|
|
3241
|
+
canvas_item["type"] = "image"
|
|
3242
|
+
canvas_item["data"] = data # base64 image data
|
|
3243
|
+
elif display_type == "plotly":
|
|
3244
|
+
canvas_item["type"] = "plotly"
|
|
3245
|
+
canvas_item["data"] = data # Plotly JSON
|
|
3246
|
+
elif display_type == "dataframe":
|
|
3247
|
+
canvas_item["type"] = "dataframe"
|
|
3248
|
+
canvas_item["data"] = display_data.get("csv", {}).get("data", [])
|
|
3249
|
+
canvas_item["columns"] = display_data.get("csv", {}).get("columns", [])
|
|
3250
|
+
canvas_item["html"] = display_data.get("csv", {}).get("html", "")
|
|
3251
|
+
elif display_type == "pdf":
|
|
3252
|
+
canvas_item["type"] = "pdf"
|
|
3253
|
+
canvas_item["data"] = data # base64 PDF data
|
|
3254
|
+
canvas_item["mime_type"] = display_data.get("mime_type", "application/pdf")
|
|
3255
|
+
elif display_type == "html":
|
|
3256
|
+
canvas_item["type"] = "markdown"
|
|
3257
|
+
canvas_item["data"] = data # Store HTML as markdown (will render)
|
|
3258
|
+
elif display_type == "json":
|
|
3259
|
+
canvas_item["type"] = "markdown"
|
|
3260
|
+
canvas_item["data"] = f"```json\n{json.dumps(data, indent=2)}\n```"
|
|
3261
|
+
else:
|
|
3262
|
+
# text or other
|
|
3263
|
+
canvas_item["type"] = "markdown"
|
|
3264
|
+
canvas_item["data"] = str(data) if data else ""
|
|
3265
|
+
|
|
3266
|
+
# Add item to canvas (session-specific in virtual FS mode)
|
|
3267
|
+
if USE_VIRTUAL_FS and session_id:
|
|
3268
|
+
current_state = _get_session_state(session_id)
|
|
3269
|
+
with _session_agents_lock:
|
|
3270
|
+
current_state["canvas"].append(canvas_item)
|
|
3271
|
+
canvas_items = current_state["canvas"].copy()
|
|
3272
|
+
else:
|
|
3273
|
+
with _agent_state_lock:
|
|
3274
|
+
_agent_state["canvas"].append(canvas_item)
|
|
3275
|
+
canvas_items = _agent_state["canvas"].copy()
|
|
3276
|
+
|
|
3277
|
+
# Export updated canvas to markdown file
|
|
3278
|
+
try:
|
|
3279
|
+
export_canvas_to_markdown(canvas_items, workspace_root)
|
|
3280
|
+
except Exception as e:
|
|
3281
|
+
print(f"Failed to export canvas after adding display item: {e}")
|
|
3282
|
+
|
|
3283
|
+
# Render updated canvas and switch to canvas view
|
|
3284
|
+
return render_canvas_items(canvas_items, colors, collapsed_ids), "canvas"
|
|
3285
|
+
|
|
3286
|
+
|
|
2763
3287
|
# =============================================================================
|
|
2764
3288
|
# THEME TOGGLE CALLBACK - Using DMC 2.4 forceColorScheme
|
|
2765
3289
|
# =============================================================================
|