Henri Bonamy commited on
Commit
3ce798d
·
1 Parent(s): 1181d83

added code panel, tool approval, tool calls logging

Browse files
agent/prompts/system_prompt.yaml CHANGED
@@ -77,7 +77,7 @@ system_prompt: |
77
 
78
  - Be concise and direct.
79
  - Don't flatter the user.
80
- - Don't use emojis nor exclamation points.
81
  - If you are limited in a task, offer alternatives.
82
  - Don't thank the user when he provides results.
83
  - Explain what you're doing for non-trivial operations.
 
77
 
78
  - Be concise and direct.
79
  - Don't flatter the user.
80
+ - Never use emojis nor exclamation points.
81
  - If you are limited in a task, offer alternatives.
82
  - Don't thank the user when he provides results.
83
  - Explain what you're doing for non-trivial operations.
configs/main_agent_config.json CHANGED
@@ -3,7 +3,7 @@
3
  "save_sessions": true,
4
  "session_dataset_repo": "akseljoonas/hf-agent-sessions",
5
  "yolo_mode": false,
6
- "confirm_cpu_jobs": false,
7
  "auto_file_upload": false,
8
  "mcpServers": {
9
  "hf-mcp-server": {
 
3
  "save_sessions": true,
4
  "session_dataset_repo": "akseljoonas/hf-agent-sessions",
5
  "yolo_mode": false,
6
+ "confirm_cpu_jobs": true,
7
  "auto_file_upload": false,
8
  "mcpServers": {
9
  "hf-mcp-server": {
frontend/package-lock.json CHANGED
@@ -15,12 +15,15 @@
15
  "react": "^18.3.1",
16
  "react-dom": "^18.3.1",
17
  "react-markdown": "^9.0.1",
 
 
18
  "zustand": "^5.0.0"
19
  },
20
  "devDependencies": {
21
  "@eslint/js": "^9.13.0",
22
  "@types/react": "^18.3.12",
23
  "@types/react-dom": "^18.3.1",
 
24
  "@vitejs/plugin-react": "^4.3.3",
25
  "eslint": "^9.13.0",
26
  "eslint-plugin-react-hooks": "^5.0.0",
@@ -1818,6 +1821,12 @@
1818
  "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
1819
  "license": "MIT"
1820
  },
 
 
 
 
 
 
1821
  "node_modules/@types/prop-types": {
1822
  "version": "15.7.15",
1823
  "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -1845,6 +1854,16 @@
1845
  "@types/react": "^18.0.0"
1846
  }
1847
  },
 
 
 
 
 
 
 
 
 
 
1848
  "node_modules/@types/react-transition-group": {
1849
  "version": "4.4.12",
1850
  "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
@@ -2856,6 +2875,19 @@
2856
  "dev": true,
2857
  "license": "MIT"
2858
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2859
  "node_modules/fdir": {
2860
  "version": "6.5.0",
2861
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2931,6 +2963,14 @@
2931
  "dev": true,
2932
  "license": "ISC"
2933
  },
 
 
 
 
 
 
 
 
2934
  "node_modules/fsevents": {
2935
  "version": "2.3.3",
2936
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3013,6 +3053,19 @@
3013
  "node": ">= 0.4"
3014
  }
3015
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3016
  "node_modules/hast-util-to-jsx-runtime": {
3017
  "version": "2.3.6",
3018
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -3053,6 +3106,38 @@
3053
  "url": "https://opencollective.com/unified"
3054
  }
3055
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3056
  "node_modules/hoist-non-react-statics": {
3057
  "version": "3.3.2",
3058
  "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -3373,6 +3458,20 @@
3373
  "loose-envify": "cli.js"
3374
  }
3375
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3376
  "node_modules/lru-cache": {
3377
  "version": "5.1.1",
3378
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3383,6 +3482,44 @@
3383
  "yallist": "^3.0.2"
3384
  }
3385
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3386
  "node_modules/mdast-util-from-markdown": {
3387
  "version": "2.0.2",
3388
  "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -3407,6 +3544,107 @@
3407
  "url": "https://opencollective.com/unified"
3408
  }
3409
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3410
  "node_modules/mdast-util-mdx-expression": {
3411
  "version": "2.0.1",
3412
  "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@@ -3605,6 +3843,127 @@
3605
  "micromark-util-types": "^2.0.0"
3606
  }
3607
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3608
  "node_modules/micromark-factory-destination": {
3609
  "version": "2.0.1",
3610
  "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@@ -4238,6 +4597,15 @@
4238
  "node": ">= 0.8.0"
4239
  }
4240
  },
 
 
 
 
 
 
 
 
 
4241
  "node_modules/prop-types": {
4242
  "version": "15.8.1",
4243
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4345,6 +4713,26 @@
4345
  "node": ">=0.10.0"
4346
  }
4347
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4348
  "node_modules/react-transition-group": {
4349
  "version": "4.4.5",
4350
  "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -4361,6 +4749,40 @@
4361
  "react-dom": ">=16.6.0"
4362
  }
4363
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4364
  "node_modules/remark-parse": {
4365
  "version": "11.0.0",
4366
  "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -4394,6 +4816,21 @@
4394
  "url": "https://opencollective.com/unified"
4395
  }
4396
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4397
  "node_modules/resolve": {
4398
  "version": "1.22.11",
4399
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
 
15
  "react": "^18.3.1",
16
  "react-dom": "^18.3.1",
17
  "react-markdown": "^9.0.1",
18
+ "react-syntax-highlighter": "^16.1.0",
19
+ "remark-gfm": "^4.0.1",
20
  "zustand": "^5.0.0"
21
  },
22
  "devDependencies": {
23
  "@eslint/js": "^9.13.0",
24
  "@types/react": "^18.3.12",
25
  "@types/react-dom": "^18.3.1",
26
+ "@types/react-syntax-highlighter": "^15.5.13",
27
  "@vitejs/plugin-react": "^4.3.3",
28
  "eslint": "^9.13.0",
29
  "eslint-plugin-react-hooks": "^5.0.0",
 
1821
  "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
1822
  "license": "MIT"
1823
  },
1824
+ "node_modules/@types/prismjs": {
1825
+ "version": "1.26.5",
1826
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
1827
+ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
1828
+ "license": "MIT"
1829
+ },
1830
  "node_modules/@types/prop-types": {
1831
  "version": "15.7.15",
1832
  "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
 
1854
  "@types/react": "^18.0.0"
1855
  }
1856
  },
1857
+ "node_modules/@types/react-syntax-highlighter": {
1858
+ "version": "15.5.13",
1859
+ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
1860
+ "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
1861
+ "dev": true,
1862
+ "license": "MIT",
1863
+ "dependencies": {
1864
+ "@types/react": "*"
1865
+ }
1866
+ },
1867
  "node_modules/@types/react-transition-group": {
1868
  "version": "4.4.12",
1869
  "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
 
2875
  "dev": true,
2876
  "license": "MIT"
2877
  },
2878
+ "node_modules/fault": {
2879
+ "version": "1.0.4",
2880
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
2881
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
2882
+ "license": "MIT",
2883
+ "dependencies": {
2884
+ "format": "^0.2.0"
2885
+ },
2886
+ "funding": {
2887
+ "type": "github",
2888
+ "url": "https://github.com/sponsors/wooorm"
2889
+ }
2890
+ },
2891
  "node_modules/fdir": {
2892
  "version": "6.5.0",
2893
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
 
2963
  "dev": true,
2964
  "license": "ISC"
2965
  },
2966
+ "node_modules/format": {
2967
+ "version": "0.2.2",
2968
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
2969
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
2970
+ "engines": {
2971
+ "node": ">=0.4.x"
2972
+ }
2973
+ },
2974
  "node_modules/fsevents": {
2975
  "version": "2.3.3",
2976
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
3053
  "node": ">= 0.4"
3054
  }
3055
  },
3056
+ "node_modules/hast-util-parse-selector": {
3057
+ "version": "4.0.0",
3058
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
3059
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
3060
+ "license": "MIT",
3061
+ "dependencies": {
3062
+ "@types/hast": "^3.0.0"
3063
+ },
3064
+ "funding": {
3065
+ "type": "opencollective",
3066
+ "url": "https://opencollective.com/unified"
3067
+ }
3068
+ },
3069
  "node_modules/hast-util-to-jsx-runtime": {
3070
  "version": "2.3.6",
3071
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
 
3106
  "url": "https://opencollective.com/unified"
3107
  }
3108
  },
3109
+ "node_modules/hastscript": {
3110
+ "version": "9.0.1",
3111
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
3112
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
3113
+ "license": "MIT",
3114
+ "dependencies": {
3115
+ "@types/hast": "^3.0.0",
3116
+ "comma-separated-tokens": "^2.0.0",
3117
+ "hast-util-parse-selector": "^4.0.0",
3118
+ "property-information": "^7.0.0",
3119
+ "space-separated-tokens": "^2.0.0"
3120
+ },
3121
+ "funding": {
3122
+ "type": "opencollective",
3123
+ "url": "https://opencollective.com/unified"
3124
+ }
3125
+ },
3126
+ "node_modules/highlight.js": {
3127
+ "version": "10.7.3",
3128
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
3129
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
3130
+ "license": "BSD-3-Clause",
3131
+ "engines": {
3132
+ "node": "*"
3133
+ }
3134
+ },
3135
+ "node_modules/highlightjs-vue": {
3136
+ "version": "1.0.0",
3137
+ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
3138
+ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
3139
+ "license": "CC0-1.0"
3140
+ },
3141
  "node_modules/hoist-non-react-statics": {
3142
  "version": "3.3.2",
3143
  "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
 
3458
  "loose-envify": "cli.js"
3459
  }
3460
  },
3461
+ "node_modules/lowlight": {
3462
+ "version": "1.20.0",
3463
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
3464
+ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
3465
+ "license": "MIT",
3466
+ "dependencies": {
3467
+ "fault": "^1.0.0",
3468
+ "highlight.js": "~10.7.0"
3469
+ },
3470
+ "funding": {
3471
+ "type": "github",
3472
+ "url": "https://github.com/sponsors/wooorm"
3473
+ }
3474
+ },
3475
  "node_modules/lru-cache": {
3476
  "version": "5.1.1",
3477
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
 
3482
  "yallist": "^3.0.2"
3483
  }
3484
  },
3485
+ "node_modules/markdown-table": {
3486
+ "version": "3.0.4",
3487
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
3488
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
3489
+ "license": "MIT",
3490
+ "funding": {
3491
+ "type": "github",
3492
+ "url": "https://github.com/sponsors/wooorm"
3493
+ }
3494
+ },
3495
+ "node_modules/mdast-util-find-and-replace": {
3496
+ "version": "3.0.2",
3497
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
3498
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
3499
+ "license": "MIT",
3500
+ "dependencies": {
3501
+ "@types/mdast": "^4.0.0",
3502
+ "escape-string-regexp": "^5.0.0",
3503
+ "unist-util-is": "^6.0.0",
3504
+ "unist-util-visit-parents": "^6.0.0"
3505
+ },
3506
+ "funding": {
3507
+ "type": "opencollective",
3508
+ "url": "https://opencollective.com/unified"
3509
+ }
3510
+ },
3511
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
3512
+ "version": "5.0.0",
3513
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
3514
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
3515
+ "license": "MIT",
3516
+ "engines": {
3517
+ "node": ">=12"
3518
+ },
3519
+ "funding": {
3520
+ "url": "https://github.com/sponsors/sindresorhus"
3521
+ }
3522
+ },
3523
  "node_modules/mdast-util-from-markdown": {
3524
  "version": "2.0.2",
3525
  "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
 
3544
  "url": "https://opencollective.com/unified"
3545
  }
3546
  },
3547
+ "node_modules/mdast-util-gfm": {
3548
+ "version": "3.1.0",
3549
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
3550
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
3551
+ "license": "MIT",
3552
+ "dependencies": {
3553
+ "mdast-util-from-markdown": "^2.0.0",
3554
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
3555
+ "mdast-util-gfm-footnote": "^2.0.0",
3556
+ "mdast-util-gfm-strikethrough": "^2.0.0",
3557
+ "mdast-util-gfm-table": "^2.0.0",
3558
+ "mdast-util-gfm-task-list-item": "^2.0.0",
3559
+ "mdast-util-to-markdown": "^2.0.0"
3560
+ },
3561
+ "funding": {
3562
+ "type": "opencollective",
3563
+ "url": "https://opencollective.com/unified"
3564
+ }
3565
+ },
3566
+ "node_modules/mdast-util-gfm-autolink-literal": {
3567
+ "version": "2.0.1",
3568
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
3569
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
3570
+ "license": "MIT",
3571
+ "dependencies": {
3572
+ "@types/mdast": "^4.0.0",
3573
+ "ccount": "^2.0.0",
3574
+ "devlop": "^1.0.0",
3575
+ "mdast-util-find-and-replace": "^3.0.0",
3576
+ "micromark-util-character": "^2.0.0"
3577
+ },
3578
+ "funding": {
3579
+ "type": "opencollective",
3580
+ "url": "https://opencollective.com/unified"
3581
+ }
3582
+ },
3583
+ "node_modules/mdast-util-gfm-footnote": {
3584
+ "version": "2.1.0",
3585
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
3586
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
3587
+ "license": "MIT",
3588
+ "dependencies": {
3589
+ "@types/mdast": "^4.0.0",
3590
+ "devlop": "^1.1.0",
3591
+ "mdast-util-from-markdown": "^2.0.0",
3592
+ "mdast-util-to-markdown": "^2.0.0",
3593
+ "micromark-util-normalize-identifier": "^2.0.0"
3594
+ },
3595
+ "funding": {
3596
+ "type": "opencollective",
3597
+ "url": "https://opencollective.com/unified"
3598
+ }
3599
+ },
3600
+ "node_modules/mdast-util-gfm-strikethrough": {
3601
+ "version": "2.0.0",
3602
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
3603
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
3604
+ "license": "MIT",
3605
+ "dependencies": {
3606
+ "@types/mdast": "^4.0.0",
3607
+ "mdast-util-from-markdown": "^2.0.0",
3608
+ "mdast-util-to-markdown": "^2.0.0"
3609
+ },
3610
+ "funding": {
3611
+ "type": "opencollective",
3612
+ "url": "https://opencollective.com/unified"
3613
+ }
3614
+ },
3615
+ "node_modules/mdast-util-gfm-table": {
3616
+ "version": "2.0.0",
3617
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
3618
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
3619
+ "license": "MIT",
3620
+ "dependencies": {
3621
+ "@types/mdast": "^4.0.0",
3622
+ "devlop": "^1.0.0",
3623
+ "markdown-table": "^3.0.0",
3624
+ "mdast-util-from-markdown": "^2.0.0",
3625
+ "mdast-util-to-markdown": "^2.0.0"
3626
+ },
3627
+ "funding": {
3628
+ "type": "opencollective",
3629
+ "url": "https://opencollective.com/unified"
3630
+ }
3631
+ },
3632
+ "node_modules/mdast-util-gfm-task-list-item": {
3633
+ "version": "2.0.0",
3634
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
3635
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
3636
+ "license": "MIT",
3637
+ "dependencies": {
3638
+ "@types/mdast": "^4.0.0",
3639
+ "devlop": "^1.0.0",
3640
+ "mdast-util-from-markdown": "^2.0.0",
3641
+ "mdast-util-to-markdown": "^2.0.0"
3642
+ },
3643
+ "funding": {
3644
+ "type": "opencollective",
3645
+ "url": "https://opencollective.com/unified"
3646
+ }
3647
+ },
3648
  "node_modules/mdast-util-mdx-expression": {
3649
  "version": "2.0.1",
3650
  "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
 
3843
  "micromark-util-types": "^2.0.0"
3844
  }
3845
  },
3846
+ "node_modules/micromark-extension-gfm": {
3847
+ "version": "3.0.0",
3848
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
3849
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
3850
+ "license": "MIT",
3851
+ "dependencies": {
3852
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
3853
+ "micromark-extension-gfm-footnote": "^2.0.0",
3854
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
3855
+ "micromark-extension-gfm-table": "^2.0.0",
3856
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
3857
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
3858
+ "micromark-util-combine-extensions": "^2.0.0",
3859
+ "micromark-util-types": "^2.0.0"
3860
+ },
3861
+ "funding": {
3862
+ "type": "opencollective",
3863
+ "url": "https://opencollective.com/unified"
3864
+ }
3865
+ },
3866
+ "node_modules/micromark-extension-gfm-autolink-literal": {
3867
+ "version": "2.1.0",
3868
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
3869
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
3870
+ "license": "MIT",
3871
+ "dependencies": {
3872
+ "micromark-util-character": "^2.0.0",
3873
+ "micromark-util-sanitize-uri": "^2.0.0",
3874
+ "micromark-util-symbol": "^2.0.0",
3875
+ "micromark-util-types": "^2.0.0"
3876
+ },
3877
+ "funding": {
3878
+ "type": "opencollective",
3879
+ "url": "https://opencollective.com/unified"
3880
+ }
3881
+ },
3882
+ "node_modules/micromark-extension-gfm-footnote": {
3883
+ "version": "2.1.0",
3884
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
3885
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
3886
+ "license": "MIT",
3887
+ "dependencies": {
3888
+ "devlop": "^1.0.0",
3889
+ "micromark-core-commonmark": "^2.0.0",
3890
+ "micromark-factory-space": "^2.0.0",
3891
+ "micromark-util-character": "^2.0.0",
3892
+ "micromark-util-normalize-identifier": "^2.0.0",
3893
+ "micromark-util-sanitize-uri": "^2.0.0",
3894
+ "micromark-util-symbol": "^2.0.0",
3895
+ "micromark-util-types": "^2.0.0"
3896
+ },
3897
+ "funding": {
3898
+ "type": "opencollective",
3899
+ "url": "https://opencollective.com/unified"
3900
+ }
3901
+ },
3902
+ "node_modules/micromark-extension-gfm-strikethrough": {
3903
+ "version": "2.1.0",
3904
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
3905
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
3906
+ "license": "MIT",
3907
+ "dependencies": {
3908
+ "devlop": "^1.0.0",
3909
+ "micromark-util-chunked": "^2.0.0",
3910
+ "micromark-util-classify-character": "^2.0.0",
3911
+ "micromark-util-resolve-all": "^2.0.0",
3912
+ "micromark-util-symbol": "^2.0.0",
3913
+ "micromark-util-types": "^2.0.0"
3914
+ },
3915
+ "funding": {
3916
+ "type": "opencollective",
3917
+ "url": "https://opencollective.com/unified"
3918
+ }
3919
+ },
3920
+ "node_modules/micromark-extension-gfm-table": {
3921
+ "version": "2.1.1",
3922
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
3923
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
3924
+ "license": "MIT",
3925
+ "dependencies": {
3926
+ "devlop": "^1.0.0",
3927
+ "micromark-factory-space": "^2.0.0",
3928
+ "micromark-util-character": "^2.0.0",
3929
+ "micromark-util-symbol": "^2.0.0",
3930
+ "micromark-util-types": "^2.0.0"
3931
+ },
3932
+ "funding": {
3933
+ "type": "opencollective",
3934
+ "url": "https://opencollective.com/unified"
3935
+ }
3936
+ },
3937
+ "node_modules/micromark-extension-gfm-tagfilter": {
3938
+ "version": "2.0.0",
3939
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
3940
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
3941
+ "license": "MIT",
3942
+ "dependencies": {
3943
+ "micromark-util-types": "^2.0.0"
3944
+ },
3945
+ "funding": {
3946
+ "type": "opencollective",
3947
+ "url": "https://opencollective.com/unified"
3948
+ }
3949
+ },
3950
+ "node_modules/micromark-extension-gfm-task-list-item": {
3951
+ "version": "2.1.0",
3952
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
3953
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
3954
+ "license": "MIT",
3955
+ "dependencies": {
3956
+ "devlop": "^1.0.0",
3957
+ "micromark-factory-space": "^2.0.0",
3958
+ "micromark-util-character": "^2.0.0",
3959
+ "micromark-util-symbol": "^2.0.0",
3960
+ "micromark-util-types": "^2.0.0"
3961
+ },
3962
+ "funding": {
3963
+ "type": "opencollective",
3964
+ "url": "https://opencollective.com/unified"
3965
+ }
3966
+ },
3967
  "node_modules/micromark-factory-destination": {
3968
  "version": "2.0.1",
3969
  "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
 
4597
  "node": ">= 0.8.0"
4598
  }
4599
  },
4600
+ "node_modules/prismjs": {
4601
+ "version": "1.30.0",
4602
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
4603
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
4604
+ "license": "MIT",
4605
+ "engines": {
4606
+ "node": ">=6"
4607
+ }
4608
+ },
4609
  "node_modules/prop-types": {
4610
  "version": "15.8.1",
4611
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
 
4713
  "node": ">=0.10.0"
4714
  }
4715
  },
4716
+ "node_modules/react-syntax-highlighter": {
4717
+ "version": "16.1.0",
4718
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz",
4719
+ "integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==",
4720
+ "license": "MIT",
4721
+ "dependencies": {
4722
+ "@babel/runtime": "^7.28.4",
4723
+ "highlight.js": "^10.4.1",
4724
+ "highlightjs-vue": "^1.0.0",
4725
+ "lowlight": "^1.17.0",
4726
+ "prismjs": "^1.30.0",
4727
+ "refractor": "^5.0.0"
4728
+ },
4729
+ "engines": {
4730
+ "node": ">= 16.20.2"
4731
+ },
4732
+ "peerDependencies": {
4733
+ "react": ">= 0.14.0"
4734
+ }
4735
+ },
4736
  "node_modules/react-transition-group": {
4737
  "version": "4.4.5",
4738
  "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
 
4749
  "react-dom": ">=16.6.0"
4750
  }
4751
  },
4752
+ "node_modules/refractor": {
4753
+ "version": "5.0.0",
4754
+ "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
4755
+ "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
4756
+ "license": "MIT",
4757
+ "dependencies": {
4758
+ "@types/hast": "^3.0.0",
4759
+ "@types/prismjs": "^1.0.0",
4760
+ "hastscript": "^9.0.0",
4761
+ "parse-entities": "^4.0.0"
4762
+ },
4763
+ "funding": {
4764
+ "type": "github",
4765
+ "url": "https://github.com/sponsors/wooorm"
4766
+ }
4767
+ },
4768
+ "node_modules/remark-gfm": {
4769
+ "version": "4.0.1",
4770
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
4771
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
4772
+ "license": "MIT",
4773
+ "dependencies": {
4774
+ "@types/mdast": "^4.0.0",
4775
+ "mdast-util-gfm": "^3.0.0",
4776
+ "micromark-extension-gfm": "^3.0.0",
4777
+ "remark-parse": "^11.0.0",
4778
+ "remark-stringify": "^11.0.0",
4779
+ "unified": "^11.0.0"
4780
+ },
4781
+ "funding": {
4782
+ "type": "opencollective",
4783
+ "url": "https://opencollective.com/unified"
4784
+ }
4785
+ },
4786
  "node_modules/remark-parse": {
4787
  "version": "11.0.0",
4788
  "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
 
4816
  "url": "https://opencollective.com/unified"
4817
  }
4818
  },
4819
+ "node_modules/remark-stringify": {
4820
+ "version": "11.0.0",
4821
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
4822
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
4823
+ "license": "MIT",
4824
+ "dependencies": {
4825
+ "@types/mdast": "^4.0.0",
4826
+ "mdast-util-to-markdown": "^2.0.0",
4827
+ "unified": "^11.0.0"
4828
+ },
4829
+ "funding": {
4830
+ "type": "opencollective",
4831
+ "url": "https://opencollective.com/unified"
4832
+ }
4833
+ },
4834
  "node_modules/resolve": {
4835
  "version": "1.22.11",
4836
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
frontend/package.json CHANGED
@@ -17,12 +17,15 @@
17
  "react": "^18.3.1",
18
  "react-dom": "^18.3.1",
19
  "react-markdown": "^9.0.1",
 
 
20
  "zustand": "^5.0.0"
21
  },
22
  "devDependencies": {
23
  "@eslint/js": "^9.13.0",
24
  "@types/react": "^18.3.12",
25
  "@types/react-dom": "^18.3.1",
 
26
  "@vitejs/plugin-react": "^4.3.3",
27
  "eslint": "^9.13.0",
28
  "eslint-plugin-react-hooks": "^5.0.0",
 
17
  "react": "^18.3.1",
18
  "react-dom": "^18.3.1",
19
  "react-markdown": "^9.0.1",
20
+ "react-syntax-highlighter": "^16.1.0",
21
+ "remark-gfm": "^4.0.1",
22
  "zustand": "^5.0.0"
23
  },
24
  "devDependencies": {
25
  "@eslint/js": "^9.13.0",
26
  "@types/react": "^18.3.12",
27
  "@types/react-dom": "^18.3.1",
28
+ "@types/react-syntax-highlighter": "^15.5.13",
29
  "@vitejs/plugin-react": "^4.3.3",
30
  "eslint": "^9.13.0",
31
  "eslint-plugin-react-hooks": "^5.0.0",
frontend/src/components/Chat/ApprovalFlow.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { Box, Typography, Button, TextField, Divider } from '@mui/material';
3
+ import { useAgentStore } from '@/store/agentStore';
4
+ import { useLayoutStore } from '@/store/layoutStore';
5
+
6
+ interface ApprovalFlowProps {
7
+ sessionId: string;
8
+ }
9
+
10
+ export default function ApprovalFlow({ sessionId }: ApprovalFlowProps) {
11
+ const { pendingApprovals, setPendingApprovals, setPanelContent } = useAgentStore();
12
+ const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
13
+ const [currentIndex, setCurrentIndex] = useState(0);
14
+ const [feedback, setFeedback] = useState('');
15
+ const [decisions, setDecisions] = useState<Array<{ tool_call_id: string; approved: boolean; feedback: string | null }>>([]);
16
+
17
+ // Reset local state when a new batch of approvals arrives
18
+ useEffect(() => {
19
+ setCurrentIndex(0);
20
+ setFeedback('');
21
+ setDecisions([]);
22
+ }, [pendingApprovals]);
23
+
24
+ // Sync right panel with current tool
25
+ useEffect(() => {
26
+ if (!pendingApprovals || currentIndex >= pendingApprovals.tools.length) return;
27
+
28
+ const tool = pendingApprovals.tools[currentIndex];
29
+ const args = tool.arguments as any;
30
+
31
+ if (tool.tool === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
32
+ setPanelContent({
33
+ title: 'Compute Job Script',
34
+ content: args.script,
35
+ language: 'python',
36
+ parameters: args
37
+ });
38
+ setRightPanelOpen(true);
39
+ setLeftSidebarOpen(false);
40
+ } else if (tool.tool === 'hf_repo_files' && args.operation === 'upload' && args.content) {
41
+ setPanelContent({
42
+ title: `File Upload: ${args.path || 'unnamed'}`,
43
+ content: args.content,
44
+ parameters: args
45
+ });
46
+ setRightPanelOpen(true);
47
+ setLeftSidebarOpen(false);
48
+ } else {
49
+ // For other tools, just show parameters in the panel
50
+ setPanelContent({
51
+ title: `Tool: ${tool.tool}`,
52
+ content: '',
53
+ parameters: args
54
+ });
55
+ }
56
+ }, [currentIndex, pendingApprovals, setPanelContent, setRightPanelOpen, setLeftSidebarOpen]);
57
+
58
+ const handleResolve = useCallback(async (approved: boolean) => {
59
+ if (!pendingApprovals) return;
60
+
61
+ const currentTool = pendingApprovals.tools[currentIndex];
62
+ const newDecisions = [
63
+ ...decisions,
64
+ {
65
+ tool_call_id: currentTool.tool_call_id,
66
+ approved,
67
+ feedback: approved ? null : feedback || 'Rejected by user',
68
+ },
69
+ ];
70
+
71
+ if (currentIndex < pendingApprovals.tools.length - 1) {
72
+ setDecisions(newDecisions);
73
+ setCurrentIndex(currentIndex + 1);
74
+ setFeedback('');
75
+ } else {
76
+ // All tools in batch resolved, submit to backend
77
+ try {
78
+ await fetch('/api/approve', {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({
82
+ session_id: sessionId,
83
+ approvals: newDecisions,
84
+ }),
85
+ });
86
+ setPendingApprovals(null);
87
+ } catch (e) {
88
+ console.error('Approval submission failed:', e);
89
+ }
90
+ }
91
+ }, [sessionId, pendingApprovals, currentIndex, feedback, decisions, setPendingApprovals]);
92
+
93
+ if (!pendingApprovals || currentIndex >= pendingApprovals.tools.length) return null;
94
+
95
+ const currentTool = pendingApprovals.tools[currentIndex];
96
+
97
+ return (
98
+ <Box sx={{
99
+ mt: 0,
100
+ mb: 4,
101
+ px: 2,
102
+ width: '100%',
103
+ alignSelf: 'center'
104
+ }}>
105
+ <Typography variant="subtitle2" sx={{ fontFamily: 'monospace', mb: 2, fontWeight: 600 }}>
106
+ ACTION REQUIRED ({currentIndex + 1}/{pendingApprovals.count}) : The agent wants to execute <Box component="span" sx={{ color: 'primary.main' }}>{currentTool.tool}</Box>
107
+ </Typography>
108
+
109
+ <Box component="pre" sx={{
110
+ bgcolor: 'background.default',
111
+ p: 1.5,
112
+ borderRadius: 0.5,
113
+ fontSize: '0.75rem',
114
+ fontFamily: 'monospace',
115
+ overflow: 'auto',
116
+ maxHeight: 150,
117
+ mb: 2,
118
+ border: 1,
119
+ borderColor: 'divider'
120
+ }}>
121
+ {JSON.stringify(currentTool.arguments, null, 2)}
122
+ </Box>
123
+
124
+ <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
125
+ <TextField
126
+ fullWidth
127
+ size="small"
128
+ placeholder="Feedback for rejection (optional)"
129
+ value={feedback}
130
+ onChange={(e) => setFeedback(e.target.value)}
131
+ variant="outlined"
132
+ sx={{
133
+ flex: 1,
134
+ '& .MuiOutlinedInput-root': { fontFamily: 'monospace', fontSize: '0.8rem', height: '36px' }
135
+ }}
136
+ />
137
+
138
+ <Button
139
+ variant="outlined"
140
+ color="error"
141
+ onClick={() => handleResolve(false)}
142
+ sx={{ fontFamily: 'monospace', height: '36px', px: 3 }}
143
+ >
144
+ REJECT
145
+ </Button>
146
+ <Button
147
+ variant="contained"
148
+ color="success"
149
+ onClick={() => handleResolve(true)}
150
+ sx={{ color: 'white', fontFamily: 'monospace', height: '36px', px: 3 }}
151
+ >
152
+ APPROVE
153
+ </Button>
154
+ </Box>
155
+ </Box>
156
+ );
157
+ }
frontend/src/components/Chat/ChatInput.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { useState, useCallback, KeyboardEvent } from 'react';
2
- import { Box, TextField, IconButton, CircularProgress } from '@mui/material';
3
- import SendIcon from '@mui/icons-material/Send';
4
 
5
  interface ChatInputProps {
6
  onSend: (text: string) => void;
@@ -30,47 +30,82 @@ export default function ChatInput({ onSend, disabled = false }: ChatInputProps)
30
  return (
31
  <Box
32
  sx={{
33
- p: 2,
34
- borderTop: 1,
35
- borderColor: 'divider',
36
- bgcolor: 'background.paper',
37
  }}
38
  >
39
- <Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
40
- <TextField
41
- fullWidth
42
- multiline
43
- maxRows={6}
44
- value={input}
45
- onChange={(e) => setInput(e.target.value)}
46
- onKeyDown={handleKeyDown}
47
- placeholder="Type a message..."
48
- disabled={disabled}
49
- variant="outlined"
50
- size="small"
51
- sx={{
52
- '& .MuiOutlinedInput-root': {
53
- bgcolor: 'background.default',
54
- },
55
- }}
56
- />
57
- <IconButton
58
- onClick={handleSend}
59
- disabled={disabled || !input.trim()}
60
- color="primary"
61
  sx={{
62
- bgcolor: 'primary.main',
63
- color: 'primary.contrastText',
64
- '&:hover': {
65
- bgcolor: 'primary.dark',
66
- },
67
- '&.Mui-disabled': {
68
- bgcolor: 'action.disabledBackground',
69
- },
 
70
  }}
71
  >
72
- {disabled ? <CircularProgress size={24} color="inherit" /> : <SendIcon />}
73
- </IconButton>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </Box>
75
  </Box>
76
  );
 
1
  import { useState, useCallback, KeyboardEvent } from 'react';
2
+ import { Box, TextField, IconButton, CircularProgress, Typography } from '@mui/material';
3
+ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
4
 
5
  interface ChatInputProps {
6
  onSend: (text: string) => void;
 
30
  return (
31
  <Box
32
  sx={{
33
+ pb: 4,
34
+ pt: 2,
35
+ position: 'relative',
36
+ zIndex: 10,
37
  }}
38
  >
39
+ <Box sx={{ maxWidth: 'md', mx: 'auto', width: '100%', px: 2 }}>
40
+ {/* Input Label and Divider */}
41
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 1, gap: 2 }}>
42
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600, color: 'text.secondary', letterSpacing: '0.05em' }}>
43
+ INPUT
44
+ </Typography>
45
+ <Box sx={{ flex: 1, height: '1px', bgcolor: 'divider' }} />
46
+ </Box>
47
+
48
+ <Box
 
 
 
 
 
 
 
 
 
 
 
 
49
  sx={{
50
+ display: 'flex',
51
+ gap: 1,
52
+ alignItems: 'center',
53
+ bgcolor: 'background.paper',
54
+ borderRadius: 1,
55
+ boxShadow: 4,
56
+ p: 1,
57
+ border: 1,
58
+ borderColor: 'divider',
59
  }}
60
  >
61
+ <TextField
62
+ fullWidth
63
+ multiline
64
+ maxRows={6}
65
+ value={input}
66
+ onChange={(e) => setInput(e.target.value)}
67
+ onKeyDown={handleKeyDown}
68
+ placeholder="Ask anything"
69
+ disabled={disabled}
70
+ variant="outlined"
71
+ size="small"
72
+ sx={{
73
+ '& .MuiOutlinedInput-root': {
74
+ padding: '9px 12px',
75
+ lineHeight: '1.4',
76
+ fontFamily: 'monospace',
77
+ },
78
+ '& .MuiInputBase-input': {
79
+ fontFamily: 'monospace',
80
+ },
81
+ '& .MuiOutlinedInput-notchedOutline': {
82
+ border: 'none',
83
+ },
84
+ }}
85
+ />
86
+ <IconButton
87
+ onClick={handleSend}
88
+ disabled={disabled || !input.trim()}
89
+ sx={{
90
+ border: 1,
91
+ borderColor: 'divider',
92
+ borderRadius: 1,
93
+ color: 'text.secondary',
94
+ transition: 'all 0.2s',
95
+ '&:hover': {
96
+ borderColor: 'primary.main',
97
+ color: 'primary.main',
98
+ bgcolor: 'transparent',
99
+ },
100
+ '&.Mui-disabled': {
101
+ borderColor: 'divider',
102
+ opacity: 0.5,
103
+ },
104
+ }}
105
+ >
106
+ {disabled ? <CircularProgress size={24} color="inherit" /> : <ArrowUpwardIcon fontSize="small" />}
107
+ </IconButton>
108
+ </Box>
109
  </Box>
110
  </Box>
111
  );
frontend/src/components/Chat/MessageBubble.tsx CHANGED
@@ -1,8 +1,6 @@
1
  import { Box, Paper, Typography, Chip } from '@mui/material';
2
  import ReactMarkdown from 'react-markdown';
3
- import PersonIcon from '@mui/icons-material/Person';
4
- import SmartToyIcon from '@mui/icons-material/SmartToy';
5
- import BuildIcon from '@mui/icons-material/Build';
6
  import type { Message } from '@/types/agent';
7
 
8
  interface MessageBubbleProps {
@@ -13,16 +11,10 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
13
  const isUser = message.role === 'user';
14
  const isTool = message.role === 'tool';
15
 
16
- const getIcon = () => {
17
- if (isUser) return <PersonIcon fontSize="small" />;
18
- if (isTool) return <BuildIcon fontSize="small" />;
19
- return <SmartToyIcon fontSize="small" />;
20
- };
21
-
22
  const getBgColor = () => {
23
- if (isUser) return 'primary.dark';
24
  if (isTool) return 'background.default';
25
- return 'background.paper';
26
  };
27
 
28
  return (
@@ -36,31 +28,59 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
36
  <Paper
37
  elevation={0}
38
  sx={{
39
- p: 2,
40
  maxWidth: isTool ? '100%' : '80%',
41
  width: isTool ? '100%' : 'auto',
42
  bgcolor: getBgColor(),
43
- border: 1,
44
  borderColor: 'divider',
 
45
  }}
46
  >
47
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
48
- {getIcon()}
49
- <Typography variant="caption" color="text.secondary">
50
- {isUser ? 'You' : isTool ? 'Tool' : 'Assistant'}
51
- </Typography>
52
- {isTool && message.toolName && (
53
- <Chip
54
- label={message.toolName}
55
- size="small"
56
- variant="outlined"
57
- sx={{ ml: 1, height: 20, fontSize: '0.7rem' }}
58
- />
59
- )}
60
- <Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>
61
- {new Date(message.timestamp).toLocaleTimeString()}
62
- </Typography>
63
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  <Box
65
  sx={{
66
  '& p': { m: 0 },
@@ -84,15 +104,34 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
84
  p: 0,
85
  },
86
  '& a': {
87
- color: 'primary.main',
 
88
  },
89
  '& ul, & ol': {
90
  pl: 2,
91
  my: 1,
92
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }}
94
  >
95
- <ReactMarkdown>{message.content}</ReactMarkdown>
96
  </Box>
97
  </Paper>
98
  </Box>
 
1
  import { Box, Paper, Typography, Chip } from '@mui/material';
2
  import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
 
 
4
  import type { Message } from '@/types/agent';
5
 
6
  interface MessageBubbleProps {
 
11
  const isUser = message.role === 'user';
12
  const isTool = message.role === 'tool';
13
 
 
 
 
 
 
 
14
  const getBgColor = () => {
15
+ if (isUser) return 'background.paper';
16
  if (isTool) return 'background.default';
17
+ return 'transparent';
18
  };
19
 
20
  return (
 
28
  <Paper
29
  elevation={0}
30
  sx={{
31
+ p: isTool ? 2 : isUser ? 1.5 : 1,
32
  maxWidth: isTool ? '100%' : '80%',
33
  width: isTool ? '100%' : 'auto',
34
  bgcolor: getBgColor(),
35
+ border: (!isUser && !isTool) ? 0 : 1,
36
  borderColor: 'divider',
37
+ borderRadius: isUser ? 2 : undefined,
38
  }}
39
  >
40
+ {isTool && (
41
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
42
+ <Typography variant="caption" color="text.secondary">
43
+ Tool
44
+ </Typography>
45
+ {message.toolName && (
46
+ <Chip
47
+ label={message.toolName}
48
+ size="small"
49
+ variant="outlined"
50
+ sx={{ ml: 1, height: 20, fontSize: '0.7rem' }}
51
+ />
52
+ )}
53
+ </Box>
54
+ )}
55
+
56
+ {/* Persisted Trace Logs */}
57
+ {message.trace && message.trace.length > 0 && (
58
+ <Box
59
+ sx={{
60
+ bgcolor: 'background.default',
61
+ borderRadius: 1,
62
+ p: 1.5,
63
+ border: 1,
64
+ borderColor: 'divider',
65
+ width: '100%',
66
+ mb: 2,
67
+ }}
68
+ >
69
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
70
+ {message.trace.map((log) => (
71
+ <Typography
72
+ key={log.id}
73
+ variant="caption"
74
+ component="div"
75
+ sx={{ color: 'common.white', fontFamily: 'monospace', fontSize: '0.75rem' }}
76
+ >
77
+ &gt; {log.text}
78
+ </Typography>
79
+ ))}
80
+ </Box>
81
+ </Box>
82
+ )}
83
+
84
  <Box
85
  sx={{
86
  '& p': { m: 0 },
 
104
  p: 0,
105
  },
106
  '& a': {
107
+ color: 'inherit',
108
+ textDecoration: 'underline',
109
  },
110
  '& ul, & ol': {
111
  pl: 2,
112
  my: 1,
113
  },
114
+ '& table': {
115
+ borderCollapse: 'collapse',
116
+ width: '100%',
117
+ my: 2,
118
+ fontSize: '0.875rem',
119
+ },
120
+ '& th': {
121
+ borderBottom: '1px solid',
122
+ borderColor: 'divider',
123
+ textAlign: 'left',
124
+ p: 1,
125
+ bgcolor: 'action.hover',
126
+ },
127
+ '& td': {
128
+ borderBottom: '1px solid',
129
+ borderColor: 'divider',
130
+ p: 1,
131
+ },
132
  }}
133
  >
134
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
135
  </Box>
136
  </Paper>
137
  </Box>
frontend/src/components/Chat/MessageList.tsx CHANGED
@@ -1,6 +1,9 @@
1
  import { useEffect, useRef } from 'react';
2
- import { Box, Typography, CircularProgress } from '@mui/material';
 
 
3
  import MessageBubble from './MessageBubble';
 
4
  import type { Message } from '@/types/agent';
5
 
6
  interface MessageListProps {
@@ -8,13 +11,51 @@ interface MessageListProps {
8
  isProcessing: boolean;
9
  }
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  export default function MessageList({ messages, isProcessing }: MessageListProps) {
12
  const bottomRef = useRef<HTMLDivElement>(null);
 
 
 
13
 
14
  // Auto-scroll to bottom when new messages arrive
15
  useEffect(() => {
16
  bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
17
- }, [messages]);
 
 
 
 
 
 
 
18
 
19
  return (
20
  <Box
@@ -24,36 +65,77 @@ export default function MessageList({ messages, isProcessing }: MessageListProps
24
  p: 2,
25
  display: 'flex',
26
  flexDirection: 'column',
27
- gap: 2,
28
  }}
29
  >
30
- {messages.length === 0 ? (
31
- <Box
32
- sx={{
33
- flex: 1,
34
- display: 'flex',
35
- alignItems: 'center',
36
- justifyContent: 'center',
37
- }}
38
- >
39
- <Typography color="text.secondary">
40
- Start a conversation by typing a message below
41
- </Typography>
42
- </Box>
43
- ) : (
44
- messages.map((message) => (
45
- <MessageBubble key={message.id} message={message} />
46
- ))
47
- )}
48
- {isProcessing && (
49
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 2 }}>
50
- <CircularProgress size={16} />
51
- <Typography variant="body2" color="text.secondary">
52
- Processing...
53
- </Typography>
54
- </Box>
55
- )}
56
- <div ref={bottomRef} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </Box>
58
  );
59
- }
 
1
  import { useEffect, useRef } from 'react';
2
+ import { Box, Typography } from '@mui/material';
3
+ import { useAgentStore } from '@/store/agentStore';
4
+ import { useSessionStore } from '@/store/sessionStore';
5
  import MessageBubble from './MessageBubble';
6
+ import ApprovalFlow from './ApprovalFlow';
7
  import type { Message } from '@/types/agent';
8
 
9
  interface MessageListProps {
 
11
  isProcessing: boolean;
12
  }
13
 
14
+ const TechnicalIndicator = () => (
15
+ <Box
16
+ component="span"
17
+ sx={{
18
+ color: 'primary.main',
19
+ fontFamily: 'monospace',
20
+ fontWeight: 'bold',
21
+ fontSize: '1.2rem',
22
+ lineHeight: 0,
23
+ display: 'inline-block',
24
+ verticalAlign: 'middle',
25
+ width: '1em',
26
+ letterSpacing: '-3px',
27
+ transform: 'scale(0.6) translateY(-2px)',
28
+ '&::after': {
29
+ content: '""',
30
+ animation: 'dots 2s steps(4, end) infinite',
31
+ },
32
+ '@keyframes dots': {
33
+ '0%': { content: '""' },
34
+ '25%': { content: '"."' },
35
+ '50%': { content: '".."' },
36
+ '75%, 100%': { content: '"..."' },
37
+ },
38
+ }}
39
+ />
40
+ );
41
+
42
  export default function MessageList({ messages, isProcessing }: MessageListProps) {
43
  const bottomRef = useRef<HTMLDivElement>(null);
44
+ const traceBoxRef = useRef<HTMLDivElement>(null);
45
+ const { traceLogs } = useAgentStore();
46
+ const { activeSessionId } = useSessionStore();
47
 
48
  // Auto-scroll to bottom when new messages arrive
49
  useEffect(() => {
50
  bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
51
+ }, [messages, isProcessing, traceLogs]);
52
+
53
+ // Auto-scroll trace box
54
+ useEffect(() => {
55
+ if (traceBoxRef.current) {
56
+ traceBoxRef.current.scrollTop = traceBoxRef.current.scrollHeight;
57
+ }
58
+ }, [traceLogs]);
59
 
60
  return (
61
  <Box
 
65
  p: 2,
66
  display: 'flex',
67
  flexDirection: 'column',
 
68
  }}
69
  >
70
+ <Box sx={{ maxWidth: 'md', mx: 'auto', width: '100%', display: 'flex', flexDirection: 'column', gap: 2 }}>
71
+ {messages.length === 0 && traceLogs.length === 0 && !isProcessing ? (
72
+ <Box
73
+ sx={{
74
+ flex: 1,
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ justifyContent: 'center',
78
+ py: 8,
79
+ }}
80
+ >
81
+ <Typography color="text.secondary" sx={{ fontFamily: 'monospace' }}>
82
+ Awaiting input…
83
+ </Typography>
84
+ </Box>
85
+ ) : (
86
+ messages.map((message) => (
87
+ <MessageBubble key={message.id} message={message} />
88
+ ))
89
+ )}
90
+
91
+ {isProcessing && (
92
+ <Box sx={{ width: '100%', mb: 2 }}>
93
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1, px: 0.5 }}>
94
+ <Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
95
+ Thinking
96
+ </Typography>
97
+ <TechnicalIndicator />
98
+ </Box>
99
+
100
+ {traceLogs.length > 0 && (
101
+ <Box
102
+ sx={{
103
+ bgcolor: 'background.default',
104
+ borderRadius: 1,
105
+ p: 2,
106
+ border: 1,
107
+ borderColor: 'divider',
108
+ width: '100%',
109
+ fontFamily: 'monospace',
110
+ maxHeight: 120,
111
+ overflowY: 'auto',
112
+ }}
113
+ ref={traceBoxRef}
114
+ >
115
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
116
+ {traceLogs.map((log) => (
117
+ <Box key={log.id}>
118
+ <Typography
119
+ variant="caption"
120
+ component="div"
121
+ sx={{ color: 'common.white', fontFamily: 'monospace' }}
122
+ >
123
+ &gt; {log.text}
124
+ </Typography>
125
+ </Box>
126
+ ))}
127
+ </Box>
128
+ </Box>
129
+ )}
130
+ </Box>
131
+ )}
132
+
133
+ {activeSessionId && (
134
+ <ApprovalFlow sessionId={activeSessionId} />
135
+ )}
136
+
137
+ <div ref={bottomRef} />
138
+ </Box>
139
  </Box>
140
  );
141
+ }
frontend/src/components/CodePanel/CodePanel.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Box, Typography, IconButton } from '@mui/material';
2
+ import CloseIcon from '@mui/icons-material/Close';
3
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
5
+ import { useAgentStore } from '@/store/agentStore';
6
+ import { useLayoutStore } from '@/store/layoutStore';
7
+
8
+ export default function CodePanel() {
9
+ const { panelContent } = useAgentStore();
10
+ const { setRightPanelOpen } = useLayoutStore();
11
+
12
+ return (
13
+ <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'background.paper' }}>
14
+ {/* Header - Always Visible */}
15
+ <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
16
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
17
+ {panelContent?.title || 'Code Panel'}
18
+ </Typography>
19
+ <IconButton size="small" onClick={() => setRightPanelOpen(false)}>
20
+ <CloseIcon fontSize="small" />
21
+ </IconButton>
22
+ </Box>
23
+
24
+ {!panelContent ? (
25
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}>
26
+ <Typography variant="body2" color="text.secondary" sx={{ fontFamily: 'monospace', opacity: 0.5 }}>
27
+ NO DATA LOADED
28
+ </Typography>
29
+ </Box>
30
+ ) : (
31
+ <Box sx={{ flex: 1, overflow: 'auto', bgcolor: 'background.default' }}>
32
+ {panelContent.content ? (
33
+ panelContent.language === 'python' ? (
34
+ <SyntaxHighlighter
35
+ language="python"
36
+ style={vscDarkPlus}
37
+ customStyle={{
38
+ margin: 0,
39
+ padding: '16px',
40
+ background: 'transparent',
41
+ fontSize: '0.8rem',
42
+ fontFamily: '"JetBrains Mono", monospace',
43
+ }}
44
+ wrapLines={true}
45
+ wrapLongLines={true}
46
+ >
47
+ {panelContent.content}
48
+ </SyntaxHighlighter>
49
+ ) : (
50
+ <Box sx={{ p: 2 }}>
51
+ <Box component="pre" sx={{
52
+ m: 0,
53
+ fontFamily: 'monospace',
54
+ fontSize: '0.8rem',
55
+ color: 'text.primary',
56
+ whiteSpace: 'pre-wrap',
57
+ wordBreak: 'break-all'
58
+ }}>
59
+ <code>{panelContent.content}</code>
60
+ </Box>
61
+ </Box>
62
+ )
63
+ ) : (
64
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4, opacity: 0.5 }}>
65
+ <Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
66
+ NO CONTENT TO DISPLAY
67
+ </Typography>
68
+ </Box>
69
+ )}
70
+ </Box>
71
+ )}
72
+ </Box>
73
+ );
74
+ }
frontend/src/components/Layout/AppLayout.tsx CHANGED
@@ -1,33 +1,72 @@
1
- import { useState, useCallback } from 'react';
2
  import {
3
  Box,
4
- AppBar,
5
- Toolbar,
6
  Typography,
7
  IconButton,
8
- Drawer,
9
- Chip,
10
  } from '@mui/material';
11
  import MenuIcon from '@mui/icons-material/Menu';
12
- import UndoIcon from '@mui/icons-material/Undo';
13
- import CompressIcon from '@mui/icons-material/Compress';
14
- import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
15
 
16
  import { useSessionStore } from '@/store/sessionStore';
17
  import { useAgentStore } from '@/store/agentStore';
 
18
  import { useAgentWebSocket } from '@/hooks/useAgentWebSocket';
19
  import SessionSidebar from '@/components/SessionSidebar/SessionSidebar';
 
20
  import ChatInput from '@/components/Chat/ChatInput';
21
  import MessageList from '@/components/Chat/MessageList';
22
- import ApprovalModal from '@/components/ApprovalModal/ApprovalModal';
23
 
24
  const DRAWER_WIDTH = 280;
25
 
26
  export default function AppLayout() {
27
- const [mobileOpen, setMobileOpen] = useState(false);
28
-
29
  const { activeSessionId } = useSessionStore();
30
- const { isConnected, isProcessing, getMessages } = useAgentStore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  const messages = activeSessionId ? getMessages(activeSessionId) : [];
33
 
@@ -37,31 +76,18 @@ export default function AppLayout() {
37
  onError: (error) => console.error('Agent error:', error),
38
  });
39
 
40
- const handleDrawerToggle = () => {
41
- setMobileOpen(!mobileOpen);
42
- };
43
-
44
- const handleUndo = useCallback(async () => {
45
- if (!activeSessionId) return;
46
- try {
47
- await fetch(`/api/undo/${activeSessionId}`, { method: 'POST' });
48
- } catch (e) {
49
- console.error('Undo failed:', e);
50
- }
51
- }, [activeSessionId]);
52
-
53
- const handleCompact = useCallback(async () => {
54
- if (!activeSessionId) return;
55
- try {
56
- await fetch(`/api/compact/${activeSessionId}`, { method: 'POST' });
57
- } catch (e) {
58
- console.error('Compact failed:', e);
59
- }
60
- }, [activeSessionId]);
61
-
62
  const handleSendMessage = useCallback(
63
  async (text: string) => {
64
  if (!activeSessionId || !text.trim()) return;
 
 
 
 
 
 
 
 
 
65
  try {
66
  await fetch('/api/submit', {
67
  method: 'POST',
@@ -75,148 +101,180 @@ export default function AppLayout() {
75
  console.error('Send failed:', e);
76
  }
77
  },
78
- [activeSessionId]
79
  );
80
 
81
- const drawer = <SessionSidebar onClose={() => setMobileOpen(false)} />;
82
-
83
  return (
84
  <Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
85
- {/* App Bar */}
86
- <AppBar
87
- position="fixed"
88
- sx={{
89
- width: { md: `calc(100% - ${DRAWER_WIDTH}px)` },
90
- ml: { md: `${DRAWER_WIDTH}px` },
91
- bgcolor: 'background.paper',
92
- borderBottom: 1,
93
- borderColor: 'divider',
94
- }}
95
- elevation={0}
96
- >
97
- <Toolbar>
98
- <IconButton
99
- color="inherit"
100
- edge="start"
101
- onClick={handleDrawerToggle}
102
- sx={{ mr: 2, display: { md: 'none' } }}
103
- >
104
- <MenuIcon />
105
- </IconButton>
106
- <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
107
- HF Agent
108
- </Typography>
109
- <Chip
110
- icon={
111
- <FiberManualRecordIcon
112
- sx={{
113
- fontSize: 12,
114
- color: isConnected ? 'success.main' : 'error.main',
115
- }}
116
- />
117
- }
118
- label={isConnected ? 'Connected' : 'Disconnected'}
119
- size="small"
120
- variant="outlined"
121
- sx={{ mr: 2 }}
122
- />
123
- <IconButton
124
- onClick={handleUndo}
125
- disabled={!activeSessionId || isProcessing}
126
- title="Undo last turn"
127
- >
128
- <UndoIcon />
129
- </IconButton>
130
- <IconButton
131
- onClick={handleCompact}
132
- disabled={!activeSessionId || isProcessing}
133
- title="Compact context"
134
- >
135
- <CompressIcon />
136
- </IconButton>
137
- </Toolbar>
138
- </AppBar>
139
-
140
- {/* Sidebar Drawer */}
141
  <Box
142
  component="nav"
143
- sx={{ width: { md: DRAWER_WIDTH }, flexShrink: { md: 0 } }}
 
 
 
 
 
144
  >
145
- {/* Mobile drawer */}
146
  <Drawer
147
- variant="temporary"
148
- open={mobileOpen}
149
- onClose={handleDrawerToggle}
150
- ModalProps={{ keepMounted: true }}
151
- sx={{
152
- display: { xs: 'block', md: 'none' },
153
- '& .MuiDrawer-paper': {
154
- boxSizing: 'border-box',
155
- width: DRAWER_WIDTH,
156
- },
157
- }}
158
- >
159
- {drawer}
160
- </Drawer>
161
- {/* Desktop drawer */}
162
- <Drawer
163
- variant="permanent"
164
  sx={{
165
  display: { xs: 'none', md: 'block' },
166
  '& .MuiDrawer-paper': {
167
  boxSizing: 'border-box',
168
  width: DRAWER_WIDTH,
 
 
 
 
169
  },
170
  }}
171
- open
172
  >
173
- {drawer}
174
  </Drawer>
175
  </Box>
176
 
177
- {/* Main Content */}
178
  <Box
179
- component="main"
180
  sx={{
181
  flexGrow: 1,
182
- width: { md: `calc(100% - ${DRAWER_WIDTH}px)` },
183
  height: '100%',
184
  display: 'flex',
185
  flexDirection: 'column',
 
 
 
186
  }}
187
  >
188
- <Toolbar /> {/* Spacer for fixed AppBar */}
189
- {activeSessionId ? (
190
- <>
191
- <MessageList messages={messages} isProcessing={isProcessing} />
192
- <ChatInput
193
- onSend={handleSendMessage}
194
- disabled={isProcessing || !isConnected}
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  />
196
- </>
197
- ) : (
198
- <Box
199
- sx={{
200
- flex: 1,
201
- display: 'flex',
202
- alignItems: 'center',
203
- justifyContent: 'center',
204
- flexDirection: 'column',
205
- gap: 2,
206
- }}
207
- >
208
- <Typography variant="h5" color="text.secondary">
209
- No session selected
210
- </Typography>
211
- <Typography variant="body2" color="text.secondary">
212
- Create a new session from the sidebar to get started
213
- </Typography>
214
  </Box>
215
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  </Box>
217
 
218
- {/* Approval Modal */}
219
- <ApprovalModal sessionId={activeSessionId} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  </Box>
221
  );
222
  }
 
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
  import {
3
  Box,
4
+ Drawer,
 
5
  Typography,
6
  IconButton,
 
 
7
  } from '@mui/material';
8
  import MenuIcon from '@mui/icons-material/Menu';
9
+ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
10
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
11
+ import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
12
 
13
  import { useSessionStore } from '@/store/sessionStore';
14
  import { useAgentStore } from '@/store/agentStore';
15
+ import { useLayoutStore } from '@/store/layoutStore';
16
  import { useAgentWebSocket } from '@/hooks/useAgentWebSocket';
17
  import SessionSidebar from '@/components/SessionSidebar/SessionSidebar';
18
+ import CodePanel from '@/components/CodePanel/CodePanel';
19
  import ChatInput from '@/components/Chat/ChatInput';
20
  import MessageList from '@/components/Chat/MessageList';
21
+ import type { Message } from '@/types/agent';
22
 
23
  const DRAWER_WIDTH = 280;
24
 
25
  export default function AppLayout() {
 
 
26
  const { activeSessionId } = useSessionStore();
27
+ const { isConnected, isProcessing, getMessages, addMessage } = useAgentStore();
28
+ const {
29
+ isLeftSidebarOpen,
30
+ isRightPanelOpen,
31
+ rightPanelWidth,
32
+ setRightPanelWidth,
33
+ toggleLeftSidebar,
34
+ toggleRightPanel
35
+ } = useLayoutStore();
36
+
37
+ const isResizing = useRef(false);
38
+
39
+ const startResizing = useCallback((e: React.MouseEvent) => {
40
+ e.preventDefault();
41
+ isResizing.current = true;
42
+ document.addEventListener('mousemove', handleMouseMove);
43
+ document.addEventListener('mouseup', stopResizing);
44
+ document.body.style.cursor = 'col-resize';
45
+ }, []);
46
+
47
+ const stopResizing = useCallback(() => {
48
+ isResizing.current = false;
49
+ document.removeEventListener('mousemove', handleMouseMove);
50
+ document.removeEventListener('mouseup', stopResizing);
51
+ document.body.style.cursor = 'default';
52
+ }, []);
53
+
54
+ const handleMouseMove = useCallback((e: MouseEvent) => {
55
+ if (!isResizing.current) return;
56
+ const newWidth = window.innerWidth - e.clientX;
57
+ const maxWidth = window.innerWidth * 0.8;
58
+ const minWidth = 300;
59
+ if (newWidth > minWidth && newWidth < maxWidth) {
60
+ setRightPanelWidth(newWidth);
61
+ }
62
+ }, [setRightPanelWidth]);
63
+
64
+ useEffect(() => {
65
+ return () => {
66
+ document.removeEventListener('mousemove', handleMouseMove);
67
+ document.removeEventListener('mouseup', stopResizing);
68
+ };
69
+ }, [handleMouseMove, stopResizing]);
70
 
71
  const messages = activeSessionId ? getMessages(activeSessionId) : [];
72
 
 
76
  onError: (error) => console.error('Agent error:', error),
77
  });
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  const handleSendMessage = useCallback(
80
  async (text: string) => {
81
  if (!activeSessionId || !text.trim()) return;
82
+
83
+ const userMsg: Message = {
84
+ id: `user_${Date.now()}`,
85
+ role: 'user',
86
+ content: text.trim(),
87
+ timestamp: new Date().toISOString(),
88
+ };
89
+ addMessage(activeSessionId, userMsg);
90
+
91
  try {
92
  await fetch('/api/submit', {
93
  method: 'POST',
 
101
  console.error('Send failed:', e);
102
  }
103
  },
104
+ [activeSessionId, addMessage]
105
  );
106
 
 
 
107
  return (
108
  <Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
109
+ {/* Left Sidebar Drawer */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  <Box
111
  component="nav"
112
+ sx={{
113
+ width: { md: isLeftSidebarOpen ? DRAWER_WIDTH : 0 },
114
+ flexShrink: { md: 0 },
115
+ transition: isResizing.current ? 'none' : 'width 0.2s',
116
+ overflow: 'hidden',
117
+ }}
118
  >
 
119
  <Drawer
120
+ variant="persistent"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  sx={{
122
  display: { xs: 'none', md: 'block' },
123
  '& .MuiDrawer-paper': {
124
  boxSizing: 'border-box',
125
  width: DRAWER_WIDTH,
126
+ borderRight: '1px solid',
127
+ borderColor: 'divider',
128
+ top: '40px', // Below logo bar
129
+ height: 'calc(100% - 40px)',
130
  },
131
  }}
132
+ open={isLeftSidebarOpen}
133
  >
134
+ <SessionSidebar />
135
  </Drawer>
136
  </Box>
137
 
138
+ {/* Main Content Area */}
139
  <Box
 
140
  sx={{
141
  flexGrow: 1,
 
142
  height: '100%',
143
  display: 'flex',
144
  flexDirection: 'column',
145
+ transition: isResizing.current ? 'none' : 'width 0.2s',
146
+ position: 'relative',
147
+ overflow: 'hidden',
148
  }}
149
  >
150
+ {/* Top Header Bar (Fixed) */}
151
+ <Box sx={{
152
+ height: '60px',
153
+ px: 1,
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ borderBottom: 1,
157
+ borderColor: 'divider',
158
+ bgcolor: 'background.default',
159
+ zIndex: 1200,
160
+ }}>
161
+ <IconButton onClick={toggleLeftSidebar} size="small">
162
+ {isLeftSidebarOpen ? <ChevronLeftIcon /> : <MenuIcon />}
163
+ </IconButton>
164
+
165
+ <Box sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
166
+ <img
167
+ src="/hf-logo-white.png"
168
+ alt="Hugging Face"
169
+ style={{ height: '40px', objectFit: 'contain' }}
170
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </Box>
172
+
173
+ <IconButton
174
+ onClick={toggleRightPanel}
175
+ size="small"
176
+ sx={{ visibility: isRightPanelOpen ? 'hidden' : 'visible' }}
177
+ >
178
+ <MenuIcon />
179
+ </IconButton>
180
+ </Box>
181
+
182
+ <Box
183
+ component="main"
184
+ sx={{
185
+ flexGrow: 1,
186
+ display: 'flex',
187
+ flexDirection: 'column',
188
+ overflow: 'hidden',
189
+ }}
190
+ >
191
+ {activeSessionId ? (
192
+ <>
193
+ <MessageList messages={messages} isProcessing={isProcessing} />
194
+ <ChatInput
195
+ onSend={handleSendMessage}
196
+ disabled={isProcessing || !isConnected}
197
+ />
198
+ </>
199
+ ) : (
200
+ <Box
201
+ sx={{
202
+ flex: 1,
203
+ display: 'flex',
204
+ alignItems: 'center',
205
+ justifyContent: 'center',
206
+ flexDirection: 'column',
207
+ gap: 2,
208
+ }}
209
+ >
210
+ <Typography variant="h5" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
211
+ NO SESSION SELECTED
212
+ </Typography>
213
+ <Typography variant="body2" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
214
+ Initialize a session via the sidebar
215
+ </Typography>
216
+ </Box>
217
+ )}
218
+ </Box>
219
  </Box>
220
 
221
+ {/* Resize Handle */}
222
+ {isRightPanelOpen && (
223
+ <Box
224
+ onMouseDown={startResizing}
225
+ sx={{
226
+ width: '4px',
227
+ cursor: 'col-resize',
228
+ bgcolor: 'divider',
229
+ display: 'flex',
230
+ alignItems: 'center',
231
+ justifyContent: 'center',
232
+ transition: 'background-color 0.2s',
233
+ zIndex: 1300,
234
+ overflow: 'hidden',
235
+ '&:hover': {
236
+ bgcolor: 'primary.main',
237
+ },
238
+ }}
239
+ >
240
+ <DragIndicatorIcon
241
+ sx={{
242
+ fontSize: '0.8rem',
243
+ color: 'text.secondary',
244
+ pointerEvents: 'none',
245
+ }}
246
+ />
247
+ </Box>
248
+ )}
249
+
250
+ {/* Right Panel Drawer */}
251
+ <Box
252
+ component="nav"
253
+ sx={{
254
+ width: { md: isRightPanelOpen ? rightPanelWidth : 0 },
255
+ flexShrink: { md: 0 },
256
+ transition: isResizing.current ? 'none' : 'width 0.2s',
257
+ overflow: 'hidden',
258
+ }}
259
+ >
260
+ <Drawer
261
+ anchor="right"
262
+ variant="persistent"
263
+ sx={{
264
+ display: { xs: 'none', md: 'block' },
265
+ '& .MuiDrawer-paper': {
266
+ boxSizing: 'border-box',
267
+ width: rightPanelWidth,
268
+ borderLeft: 'none',
269
+ top: '40px', // Below logo bar
270
+ height: 'calc(100% - 40px)',
271
+ },
272
+ }}
273
+ open={isRightPanelOpen}
274
+ >
275
+ <CodePanel />
276
+ </Drawer>
277
+ </Box>
278
  </Box>
279
  );
280
  }
frontend/src/components/SessionSidebar/SessionSidebar.tsx CHANGED
@@ -11,10 +11,11 @@ import {
11
  Button,
12
  Divider,
13
  Chip,
 
14
  } from '@mui/material';
15
- import AddIcon from '@mui/icons-material/Add';
16
  import DeleteIcon from '@mui/icons-material/Delete';
17
- import ChatIcon from '@mui/icons-material/Chat';
 
18
  import { useSessionStore } from '@/store/sessionStore';
19
  import { useAgentStore } from '@/store/agentStore';
20
 
@@ -22,10 +23,37 @@ interface SessionSidebarProps {
22
  onClose?: () => void;
23
  }
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  export default function SessionSidebar({ onClose }: SessionSidebarProps) {
26
  const { sessions, activeSessionId, createSession, deleteSession, switchSession } =
27
  useSessionStore();
28
- const { clearMessages } = useAgentStore();
29
 
30
  const handleNewSession = useCallback(async () => {
31
  try {
@@ -60,30 +88,46 @@ export default function SessionSidebar({ onClose }: SessionSidebarProps) {
60
  [switchSession, onClose]
61
  );
62
 
63
- const formatDate = (dateString: string) => {
64
- const date = new Date(dateString);
65
- const now = new Date();
66
- const isToday = date.toDateString() === now.toDateString();
67
- if (isToday) {
68
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
69
  }
70
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
 
 
 
71
  };
72
 
73
  return (
74
  <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
75
  {/* Header */}
76
- <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
77
- <Typography variant="h6" sx={{ mb: 2 }}>
78
- Sessions
79
- </Typography>
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  <Button
81
  fullWidth
82
- variant="contained"
83
- startIcon={<AddIcon />}
84
  onClick={handleNewSession}
 
85
  >
86
- New Session
87
  </Button>
88
  </Box>
89
 
@@ -91,70 +135,81 @@ export default function SessionSidebar({ onClose }: SessionSidebarProps) {
91
  <Box sx={{ flex: 1, overflow: 'auto' }}>
92
  {sessions.length === 0 ? (
93
  <Box sx={{ p: 3, textAlign: 'center' }}>
94
- <ChatIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 1 }} />
95
- <Typography variant="body2" color="text.secondary">
96
- No sessions yet
97
  </Typography>
98
  <Typography variant="caption" color="text.secondary">
99
- Create a new session to get started
100
  </Typography>
101
  </Box>
102
  ) : (
103
  <List disablePadding>
104
- {[...sessions].reverse().map((session) => (
105
- <ListItem key={session.id} disablePadding divider>
106
- <ListItemButton
107
- selected={session.id === activeSessionId}
108
- onClick={() => handleSelectSession(session.id)}
109
- sx={{
110
- '&.Mui-selected': {
111
- bgcolor: 'action.selected',
112
- '&:hover': {
113
  bgcolor: 'action.selected',
 
 
 
114
  },
115
- },
116
- }}
117
- >
118
- <ListItemText
119
- primary={
120
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
121
- <Typography variant="body2" noWrap>
122
- {session.title}
 
 
 
 
 
 
 
123
  </Typography>
124
- {session.isActive && (
125
- <Chip
126
- label="active"
127
- size="small"
128
- color="success"
129
- sx={{ height: 18, fontSize: '0.65rem' }}
130
- />
131
- )}
132
- </Box>
133
- }
134
- secondary={formatDate(session.createdAt)}
135
- />
136
- <ListItemSecondaryAction>
137
- <IconButton
138
- edge="end"
139
- size="small"
140
- onClick={(e) => handleDeleteSession(session.id, e)}
141
- >
142
- <DeleteIcon fontSize="small" />
143
- </IconButton>
144
- </ListItemSecondaryAction>
145
- </ListItemButton>
146
- </ListItem>
147
- ))}
148
  </List>
149
  )}
150
  </Box>
151
 
152
  {/* Footer */}
153
  <Divider />
154
- <Box sx={{ p: 2 }}>
155
- <Typography variant="caption" color="text.secondary">
156
- {sessions.length} session{sessions.length !== 1 ? 's' : ''}
157
  </Typography>
 
 
 
 
 
 
 
 
 
 
 
158
  </Box>
159
  </Box>
160
  );
 
11
  Button,
12
  Divider,
13
  Chip,
14
+ Tooltip,
15
  } from '@mui/material';
 
16
  import DeleteIcon from '@mui/icons-material/Delete';
17
+ import UndoIcon from '@mui/icons-material/Undo';
18
+ import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
19
  import { useSessionStore } from '@/store/sessionStore';
20
  import { useAgentStore } from '@/store/agentStore';
21
 
 
23
  onClose?: () => void;
24
  }
25
 
26
+ const StatusDiode = ({ connected }: { connected: boolean }) => (
27
+ <Box
28
+ sx={{
29
+ width: 8,
30
+ height: 8,
31
+ borderRadius: '50%',
32
+ bgcolor: connected ? 'success.main' : 'error.main',
33
+ boxShadow: connected ? '0 0 0 0 rgba(46, 160, 67, 0.7)' : 'none',
34
+ animation: connected ? 'pulse 2s infinite' : 'none',
35
+ '@keyframes pulse': {
36
+ '0%': {
37
+ transform: 'scale(0.95)',
38
+ boxShadow: '0 0 0 0 rgba(46, 160, 67, 0.7)',
39
+ },
40
+ '70%': {
41
+ transform: 'scale(1)',
42
+ boxShadow: '0 0 0 4px rgba(46, 160, 67, 0)',
43
+ },
44
+ '100%': {
45
+ transform: 'scale(0.95)',
46
+ boxShadow: '0 0 0 0 rgba(46, 160, 67, 0)',
47
+ },
48
+ },
49
+ }}
50
+ />
51
+ );
52
+
53
  export default function SessionSidebar({ onClose }: SessionSidebarProps) {
54
  const { sessions, activeSessionId, createSession, deleteSession, switchSession } =
55
  useSessionStore();
56
+ const { clearMessages, isConnected, isProcessing } = useAgentStore();
57
 
58
  const handleNewSession = useCallback(async () => {
59
  try {
 
88
  [switchSession, onClose]
89
  );
90
 
91
+ const handleUndo = useCallback(async () => {
92
+ if (!activeSessionId) return;
93
+ try {
94
+ await fetch(`/api/undo/${activeSessionId}`, { method: 'POST' });
95
+ } catch (e) {
96
+ console.error('Undo failed:', e);
97
  }
98
+ }, [activeSessionId]);
99
+
100
+ const formatTime = (dateString: string) => {
101
+ return new Date(dateString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
102
  };
103
 
104
  return (
105
  <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
106
  {/* Header */}
107
+ <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
108
+ <Box sx={{ mb: 2 }}>
109
+ <img
110
+ src="/hf-log-only-white.png"
111
+ alt="HF Agent"
112
+ style={{ height: '32px', objectFit: 'contain' }}
113
+ />
114
+ </Box>
115
+
116
+ {/* System Info / Status */}
117
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
118
+ <Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
119
+ {isConnected ? 'Connected' : 'Disconnected'}
120
+ </Typography>
121
+ <StatusDiode connected={isConnected} />
122
+ </Box>
123
+
124
  <Button
125
  fullWidth
126
+ variant="outlined"
 
127
  onClick={handleNewSession}
128
+ sx={{ justifyContent: 'center' }}
129
  >
130
+ Create Session
131
  </Button>
132
  </Box>
133
 
 
135
  <Box sx={{ flex: 1, overflow: 'auto' }}>
136
  {sessions.length === 0 ? (
137
  <Box sx={{ p: 3, textAlign: 'center' }}>
138
+ <Typography variant="body2" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
139
+ NO ACTIVE SESSIONS
 
140
  </Typography>
141
  <Typography variant="caption" color="text.secondary">
142
+ Initialize a new session to begin
143
  </Typography>
144
  </Box>
145
  ) : (
146
  <List disablePadding>
147
+ {[...sessions].reverse().map((session, index) => {
148
+ const sessionNumber = sessions.length - index;
149
+ return (
150
+ <ListItem key={session.id} disablePadding divider>
151
+ <ListItemButton
152
+ selected={session.id === activeSessionId}
153
+ onClick={() => handleSelectSession(session.id)}
154
+ sx={{
155
+ '&.Mui-selected': {
156
  bgcolor: 'action.selected',
157
+ '&:hover': {
158
+ bgcolor: 'action.selected',
159
+ },
160
  },
161
+ }}
162
+ >
163
+ <ListItemText
164
+ primary={
165
+ <Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
166
+ SESSION {String(sessionNumber).padStart(3, '0')}
167
+ </Typography>
168
+ }
169
+ secondary={
170
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: 1 }}>
171
+ <span style={{ color: session.isActive ? 'var(--mui-palette-success-main)' : 'var(--mui-palette-text-secondary)' }}>
172
+ {session.isActive ? 'RUNNING' : 'STOPPED'}
173
+ </span>
174
+ <span>·</span>
175
+ <span>{formatTime(session.createdAt)}</span>
176
  </Typography>
177
+ }
178
+ />
179
+ <ListItemSecondaryAction>
180
+ <IconButton
181
+ edge="end"
182
+ size="small"
183
+ onClick={(e) => handleDeleteSession(session.id, e)}
184
+ >
185
+ <DeleteIcon fontSize="small" />
186
+ </IconButton>
187
+ </ListItemSecondaryAction>
188
+ </ListItemButton>
189
+ </ListItem>
190
+ );
191
+ })}
 
 
 
 
 
 
 
 
 
192
  </List>
193
  )}
194
  </Box>
195
 
196
  {/* Footer */}
197
  <Divider />
198
+ <Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
199
+ <Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
200
+ {sessions.length} SESSION{sessions.length !== 1 ? 'S' : ''}
201
  </Typography>
202
+ <Tooltip title="Undo last turn">
203
+ <span>
204
+ <IconButton
205
+ onClick={handleUndo}
206
+ disabled={!activeSessionId || isProcessing}
207
+ size="small"
208
+ >
209
+ <UndoIcon fontSize="small" />
210
+ </IconButton>
211
+ </span>
212
+ </Tooltip>
213
  </Box>
214
  </Box>
215
  );
frontend/src/hooks/useAgentWebSocket.ts CHANGED
@@ -1,8 +1,9 @@
1
  import { useCallback, useEffect, useRef } from 'react';
2
  import { useAgentStore } from '@/store/agentStore';
3
  import { useSessionStore } from '@/store/sessionStore';
 
4
  import type { AgentEvent } from '@/types/events';
5
- import type { Message } from '@/types/agent';
6
 
7
  const WS_RECONNECT_DELAY = 1000;
8
  const WS_MAX_RECONNECT_DELAY = 30000;
@@ -28,8 +29,14 @@ export function useAgentWebSocket({
28
  setConnected,
29
  setPendingApprovals,
30
  setError,
 
 
 
 
31
  } = useAgentStore();
32
 
 
 
33
  const { setSessionActive } = useSessionStore();
34
 
35
  const handleEvent = useCallback(
@@ -46,15 +53,18 @@ export function useAgentWebSocket({
46
 
47
  case 'processing':
48
  setProcessing(true);
 
49
  break;
50
 
51
  case 'assistant_message': {
52
  const content = (event.data?.content as string) || '';
 
53
  const message: Message = {
54
  id: `msg_${Date.now()}`,
55
  role: 'assistant',
56
  content,
57
  timestamp: new Date().toISOString(),
 
58
  };
59
  addMessage(sessionId, message);
60
  break;
@@ -62,16 +72,36 @@ export function useAgentWebSocket({
62
 
63
  case 'tool_call': {
64
  const toolName = (event.data?.tool as string) || 'unknown';
65
- const args = event.data?.arguments || {};
66
- const message: Message = {
67
  id: `tool_${Date.now()}`,
68
- role: 'tool',
69
- content: `Calling ${toolName}...`,
 
70
  timestamp: new Date().toISOString(),
71
- toolName,
72
  };
73
- addMessage(sessionId, message);
74
- // Store tool call args for display
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  console.log('Tool call:', toolName, args);
76
  break;
77
  }
@@ -80,14 +110,7 @@ export function useAgentWebSocket({
80
  const toolName = (event.data?.tool as string) || 'unknown';
81
  const output = (event.data?.output as string) || '';
82
  const success = event.data?.success as boolean;
83
- const message: Message = {
84
- id: `tool_out_${Date.now()}`,
85
- role: 'tool',
86
- content: output,
87
- timestamp: new Date().toISOString(),
88
- toolName,
89
- };
90
- addMessage(sessionId, message);
91
  console.log('Tool output:', toolName, success);
92
  break;
93
  }
 
1
  import { useCallback, useEffect, useRef } from 'react';
2
  import { useAgentStore } from '@/store/agentStore';
3
  import { useSessionStore } from '@/store/sessionStore';
4
+ import { useLayoutStore } from '@/store/layoutStore';
5
  import type { AgentEvent } from '@/types/events';
6
+ import type { Message, TraceLog } from '@/types/agent';
7
 
8
  const WS_RECONNECT_DELAY = 1000;
9
  const WS_MAX_RECONNECT_DELAY = 30000;
 
29
  setConnected,
30
  setPendingApprovals,
31
  setError,
32
+ addTraceLog,
33
+ clearTraceLogs,
34
+ setPanelContent,
35
+ traceLogs,
36
  } = useAgentStore();
37
 
38
+ const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
39
+
40
  const { setSessionActive } = useSessionStore();
41
 
42
  const handleEvent = useCallback(
 
53
 
54
  case 'processing':
55
  setProcessing(true);
56
+ clearTraceLogs();
57
  break;
58
 
59
  case 'assistant_message': {
60
  const content = (event.data?.content as string) || '';
61
+ const currentTrace = useAgentStore.getState().traceLogs;
62
  const message: Message = {
63
  id: `msg_${Date.now()}`,
64
  role: 'assistant',
65
  content,
66
  timestamp: new Date().toISOString(),
67
+ trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
68
  };
69
  addMessage(sessionId, message);
70
  break;
 
72
 
73
  case 'tool_call': {
74
  const toolName = (event.data?.tool as string) || 'unknown';
75
+ const args = (event.data?.arguments as Record<string, any>) || {};
76
+ const log: TraceLog = {
77
  id: `tool_${Date.now()}`,
78
+ type: 'call',
79
+ text: `Calling ${toolName} with ${JSON.stringify(args)}`,
80
+ tool: toolName,
81
  timestamp: new Date().toISOString(),
 
82
  };
83
+ addTraceLog(log);
84
+
85
+ // Auto-expand Right Panel for specific tools
86
+ if (toolName === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
87
+ setPanelContent({
88
+ title: 'Compute Job Script',
89
+ content: args.script,
90
+ language: 'python'
91
+ });
92
+ setRightPanelOpen(true);
93
+ setLeftSidebarOpen(false);
94
+ } else if (toolName === 'hf_repo_files' && args.operation === 'upload' && args.content) {
95
+ setPanelContent({
96
+ title: `File Upload: ${args.path || 'unnamed'} `,
97
+ content: args.content,
98
+ parameters: args,
99
+ language: args.path?.endsWith('.py') ? 'python' : undefined
100
+ });
101
+ setRightPanelOpen(true);
102
+ setLeftSidebarOpen(false);
103
+ }
104
+
105
  console.log('Tool call:', toolName, args);
106
  break;
107
  }
 
110
  const toolName = (event.data?.tool as string) || 'unknown';
111
  const output = (event.data?.output as string) || '';
112
  const success = event.data?.success as boolean;
113
+ // Only log output to console, not to trace logs per user request
 
 
 
 
 
 
 
114
  console.log('Tool output:', toolName, success);
115
  break;
116
  }
frontend/src/store/agentStore.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { create } from 'zustand';
2
- import type { Message, ApprovalBatch, User } from '@/types/agent';
3
 
4
  interface AgentStore {
5
  // State per session (keyed by session ID)
@@ -9,6 +9,8 @@ interface AgentStore {
9
  pendingApprovals: ApprovalBatch | null;
10
  user: User | null;
11
  error: string | null;
 
 
12
 
13
  // Actions
14
  addMessage: (sessionId: string, message: Message) => void;
@@ -19,6 +21,9 @@ interface AgentStore {
19
  setUser: (user: User | null) => void;
20
  setError: (error: string | null) => void;
21
  getMessages: (sessionId: string) => Message[];
 
 
 
22
  }
23
 
24
  export const useAgentStore = create<AgentStore>((set, get) => ({
@@ -28,6 +33,8 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
28
  pendingApprovals: null,
29
  user: null,
30
  error: null,
 
 
31
 
32
  addMessage: (sessionId: string, message: Message) => {
33
  set((state) => {
@@ -73,4 +80,18 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
73
  getMessages: (sessionId: string) => {
74
  return get().messagesBySession[sessionId] || [];
75
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }));
 
1
  import { create } from 'zustand';
2
+ import type { Message, ApprovalBatch, User, TraceLog } from '@/types/agent';
3
 
4
  interface AgentStore {
5
  // State per session (keyed by session ID)
 
9
  pendingApprovals: ApprovalBatch | null;
10
  user: User | null;
11
  error: string | null;
12
+ traceLogs: TraceLog[];
13
+ panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
14
 
15
  // Actions
16
  addMessage: (sessionId: string, message: Message) => void;
 
21
  setUser: (user: User | null) => void;
22
  setError: (error: string | null) => void;
23
  getMessages: (sessionId: string) => Message[];
24
+ addTraceLog: (log: TraceLog) => void;
25
+ clearTraceLogs: () => void;
26
+ setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
27
  }
28
 
29
  export const useAgentStore = create<AgentStore>((set, get) => ({
 
33
  pendingApprovals: null,
34
  user: null,
35
  error: null,
36
+ traceLogs: [],
37
+ panelContent: null,
38
 
39
  addMessage: (sessionId: string, message: Message) => {
40
  set((state) => {
 
80
  getMessages: (sessionId: string) => {
81
  return get().messagesBySession[sessionId] || [];
82
  },
83
+
84
+ addTraceLog: (log: TraceLog) => {
85
+ set((state) => ({
86
+ traceLogs: [...state.traceLogs, log],
87
+ }));
88
+ },
89
+
90
+ clearTraceLogs: () => {
91
+ set({ traceLogs: [] });
92
+ },
93
+
94
+ setPanelContent: (content) => {
95
+ set({ panelContent: content });
96
+ },
97
  }));
frontend/src/store/layoutStore.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand';
2
+
3
+ interface LayoutStore {
4
+ isLeftSidebarOpen: boolean;
5
+ isRightPanelOpen: boolean;
6
+ rightPanelWidth: number;
7
+ setLeftSidebarOpen: (open: boolean) => void;
8
+ setRightPanelOpen: (open: boolean) => void;
9
+ setRightPanelWidth: (width: number) => void;
10
+ toggleLeftSidebar: () => void;
11
+ toggleRightPanel: () => void;
12
+ }
13
+
14
+ export const useLayoutStore = create<LayoutStore>((set) => ({
15
+ isLeftSidebarOpen: true,
16
+ isRightPanelOpen: false,
17
+ rightPanelWidth: 450,
18
+ setLeftSidebarOpen: (open) => set({ isLeftSidebarOpen: open }),
19
+ setRightPanelOpen: (open) => set({ isRightPanelOpen: open }),
20
+ setRightPanelWidth: (width) => set({ rightPanelWidth: width }),
21
+ toggleLeftSidebar: () => set((state) => ({ isLeftSidebarOpen: !state.isLeftSidebarOpen })),
22
+ toggleRightPanel: () => set((state) => ({ isRightPanelOpen: !state.isRightPanelOpen })),
23
+ }));
frontend/src/theme.ts CHANGED
@@ -4,9 +4,9 @@ const theme = createTheme({
4
  palette: {
5
  mode: 'dark',
6
  primary: {
7
- main: '#FFD21E',
8
- light: '#FFE066',
9
- dark: '#E6BD1B',
10
  },
11
  secondary: {
12
  main: '#FF9D00',
@@ -21,7 +21,7 @@ const theme = createTheme({
21
  },
22
  divider: '#30363D',
23
  success: {
24
- main: '#3FB950',
25
  },
26
  error: {
27
  main: '#F85149',
@@ -34,7 +34,7 @@ const theme = createTheme({
34
  },
35
  },
36
  typography: {
37
- fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
38
  h1: {
39
  fontWeight: 600,
40
  },
@@ -59,6 +59,9 @@ const theme = createTheme({
59
  body2: {
60
  fontSize: '0.875rem',
61
  },
 
 
 
62
  },
63
  components: {
64
  MuiCssBaseline: {
@@ -71,7 +74,7 @@ const theme = createTheme({
71
  },
72
  '&::-webkit-scrollbar-thumb': {
73
  backgroundColor: '#30363D',
74
- borderRadius: '4px',
75
  },
76
  '&::-webkit-scrollbar-track': {
77
  backgroundColor: 'transparent',
@@ -85,8 +88,10 @@ const theme = createTheme({
85
  MuiButton: {
86
  styleOverrides: {
87
  root: {
88
- textTransform: 'none',
89
- fontWeight: 500,
 
 
90
  },
91
  },
92
  },
@@ -106,7 +111,7 @@ const theme = createTheme({
106
  },
107
  },
108
  shape: {
109
- borderRadius: 8,
110
  },
111
  });
112
 
 
4
  palette: {
5
  mode: 'dark',
6
  primary: {
7
+ main: '#FEE133',
8
+ light: '#FFF066',
9
+ dark: '#B29F24',
10
  },
11
  secondary: {
12
  main: '#FF9D00',
 
21
  },
22
  divider: '#30363D',
23
  success: {
24
+ main: '#2EA043', // Muted green
25
  },
26
  error: {
27
  main: '#F85149',
 
34
  },
35
  },
36
  typography: {
37
+ fontFamily: '"IBM Plex Sans", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
38
  h1: {
39
  fontWeight: 600,
40
  },
 
59
  body2: {
60
  fontSize: '0.875rem',
61
  },
62
+ button: {
63
+ fontFamily: '"JetBrains Mono", "IBM Plex Sans", monospace',
64
+ },
65
  },
66
  components: {
67
  MuiCssBaseline: {
 
74
  },
75
  '&::-webkit-scrollbar-thumb': {
76
  backgroundColor: '#30363D',
77
+ borderRadius: '2px',
78
  },
79
  '&::-webkit-scrollbar-track': {
80
  backgroundColor: 'transparent',
 
88
  MuiButton: {
89
  styleOverrides: {
90
  root: {
91
+ textTransform: 'uppercase',
92
+ fontWeight: 600,
93
+ letterSpacing: '0.05em',
94
+ fontSize: '0.75rem',
95
  },
96
  },
97
  },
 
111
  },
112
  },
113
  shape: {
114
+ borderRadius: 2,
115
  },
116
  });
117
 
frontend/src/types/agent.ts CHANGED
@@ -16,6 +16,7 @@ export interface Message {
16
  timestamp: string;
17
  toolName?: string;
18
  toolCallId?: string;
 
19
  }
20
 
21
  export interface ToolCall {
@@ -41,6 +42,14 @@ export interface ApprovalBatch {
41
  count: number;
42
  }
43
 
 
 
 
 
 
 
 
 
44
  export interface User {
45
  authenticated: boolean;
46
  username?: string;
 
16
  timestamp: string;
17
  toolName?: string;
18
  toolCallId?: string;
19
+ trace?: TraceLog[];
20
  }
21
 
22
  export interface ToolCall {
 
42
  count: number;
43
  }
44
 
45
+ export interface TraceLog {
46
+ id: string;
47
+ type: 'call' | 'output';
48
+ text: string;
49
+ tool: string;
50
+ timestamp: string;
51
+ }
52
+
53
  export interface User {
54
  authenticated: boolean;
55
  username?: string;