mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 14:22:02 +00:00
Compare commits
992 Commits
test-relea
...
feat/debug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78d08c2b5b | ||
|
|
abab7be367 | ||
|
|
73d0edb873 | ||
|
|
84d93c2901 | ||
|
|
d558050dfa | ||
|
|
5991e99853 | ||
|
|
9661aa1dad | ||
|
|
d4649ec456 | ||
|
|
fde9eea2d6 | ||
|
|
d1e3251c29 | ||
|
|
2f51991558 | ||
|
|
7963525246 | ||
|
|
feae1d7686 | ||
|
|
bbdfaf6463 | ||
|
|
e4d86aa654 | ||
|
|
4ac1edf314 | ||
|
|
4f3ac27534 | ||
|
|
4a41dbb665 | ||
|
|
f90cd61048 | ||
|
|
078f107f66 | ||
|
|
64642916ab | ||
|
|
e2206d7a96 | ||
|
|
32f859b927 | ||
|
|
ac92725a6c | ||
|
|
5c95d6d58e | ||
|
|
abddfad063 | ||
|
|
3512749e3c | ||
|
|
2c70835769 | ||
|
|
b1f7139bb6 | ||
|
|
22aa24ae04 | ||
|
|
586aabe11f | ||
|
|
afb0937cb3 | ||
|
|
d677910f40 | ||
|
|
6d41c7d0bc | ||
|
|
9552670d3d | ||
|
|
e32a82cca5 | ||
|
|
019d6dd7bd | ||
|
|
c6d94d4bf4 | ||
|
|
ef06c13c1a | ||
|
|
3ed3a90bf6 | ||
|
|
ff281e23d0 | ||
|
|
f34fd955ac | ||
|
|
46cb6fa425 | ||
|
|
818d8af998 | ||
|
|
88aba360e3 | ||
|
|
ec6d36bda5 | ||
|
|
816bf8f6f6 | ||
|
|
a6d665c4fa | ||
|
|
d13a16111c | ||
|
|
8d5e7b068c | ||
|
|
6d4f28575f | ||
|
|
7596ff9ec3 | ||
|
|
35441c1a9d | ||
|
|
abed3b3d75 | ||
|
|
9071f89ec8 | ||
|
|
3c8ee5b714 | ||
|
|
8e1a9addc1 | ||
|
|
e72f7d1e1a | ||
|
|
4a28b70b72 | ||
|
|
3e95a11189 | ||
|
|
2b942a6cb1 | ||
|
|
69f3ba9724 | ||
|
|
96a999817f | ||
|
|
8c04e0028f | ||
|
|
81d300391d | ||
|
|
d417666fe1 | ||
|
|
2bbc8113c0 | ||
|
|
7e03af2dc6 | ||
|
|
914734cff6 | ||
|
|
e1bdb4c7df | ||
|
|
ad947691df | ||
|
|
ab9ef0d560 | ||
|
|
844be657c8 | ||
|
|
90c89ef338 | ||
|
|
fb46c0c9ea | ||
|
|
81bd57cf6a | ||
|
|
83e59d6a4d | ||
|
|
59d47928a7 | ||
|
|
cbe951dd8f | ||
|
|
63b9f52d6b | ||
|
|
3b3e61da8d | ||
|
|
0e22098652 | ||
|
|
cf9a1f9077 | ||
|
|
9b1174408b | ||
|
|
207fd26681 | ||
|
|
aa318099dc | ||
|
|
7dec5d9d74 | ||
|
|
17dae1571b | ||
|
|
f56b873571 | ||
|
|
bd432b1da3 | ||
|
|
b51aed849c | ||
|
|
90e62b8add | ||
|
|
67c6c9a9e7 | ||
|
|
2d66e38fa7 | ||
|
|
50aac1c218 | ||
|
|
8c8a4875ca | ||
|
|
eec36268fe | ||
|
|
f6efbd1b26 | ||
|
|
019793e047 | ||
|
|
a8a3711246 | ||
|
|
b867ca1407 | ||
|
|
75143c0792 | ||
|
|
f32f3e82b2 | ||
|
|
abe272ef4d | ||
|
|
6d4ab9cc13 | ||
|
|
98381441b9 | ||
|
|
eae60ab6b9 | ||
|
|
1d7b64cea8 | ||
|
|
6337e266c5 | ||
|
|
da38adcba6 | ||
|
|
af493fb73e | ||
|
|
79bf1c9bec | ||
|
|
b9a6e29ee8 | ||
|
|
2828431cca | ||
|
|
d3f46f565b | ||
|
|
3f4f2199eb | ||
|
|
38f0b16530 | ||
|
|
bd22323149 | ||
|
|
f6ce03d59a | ||
|
|
63816043cf | ||
|
|
eafe474dbc | ||
|
|
59bbbd43c5 | ||
|
|
2b89b0606c | ||
|
|
f496bb825d | ||
|
|
07327e48b4 | ||
|
|
e818922b0d | ||
|
|
04aac7ec07 | ||
|
|
944e2f5ffe | ||
|
|
9653e2b970 | ||
|
|
5c400b7eff | ||
|
|
3bc4b7f1f3 | ||
|
|
d539f7e3b7 | ||
|
|
853292af45 | ||
|
|
3c6736bc44 | ||
|
|
dac916496c | ||
|
|
847a8ff327 | ||
|
|
504c19aef5 | ||
|
|
ed2da7932c | ||
|
|
078ab943a8 | ||
|
|
948fdb6352 | ||
|
|
b0f83b7c76 | ||
|
|
38d0e4103a | ||
|
|
19016f03d7 | ||
|
|
26e4ac0d2f | ||
|
|
efd9a1b7d9 | ||
|
|
968d889346 | ||
|
|
ed66fdd57d | ||
|
|
34e51ddc3d | ||
|
|
68cefe43fb | ||
|
|
d6a1c08952 | ||
|
|
fd7c22a457 | ||
|
|
0798a64cd6 | ||
|
|
fcba327fdb | ||
|
|
4d69d04e2b | ||
|
|
f43e90f2d2 | ||
|
|
ac0d4a556a | ||
|
|
2be0e7d5f0 | ||
|
|
24599e0b8c | ||
|
|
45d93f28bf | ||
|
|
04aca1c8cb | ||
|
|
784d7fc059 | ||
|
|
d6705fbfb5 | ||
|
|
c5ae9ad262 | ||
|
|
5a0ad75059 | ||
|
|
cf62dbbf7a | ||
|
|
a4d1a1497a | ||
|
|
b798260491 | ||
|
|
1fcaa52f72 | ||
|
|
46caae05d2 | ||
|
|
39f2c8c9ff | ||
|
|
59a6a23f9b | ||
|
|
3d655c3298 | ||
|
|
2ba114931c | ||
|
|
88bb5b923f | ||
|
|
a415ae6207 | ||
|
|
504d9aa9d7 | ||
|
|
ab0cd95d9a | ||
|
|
4c65855140 | ||
|
|
adfc353b2d | ||
|
|
c1c2e706f0 | ||
|
|
d5aea8355b | ||
|
|
e498f39153 | ||
|
|
4157e11bba | ||
|
|
d66259b411 | ||
|
|
677f441cd1 | ||
|
|
e556521c8d | ||
|
|
dc8c06e447 | ||
|
|
e448d6d4e5 | ||
|
|
55bd9b0dc7 | ||
|
|
b76f09db2d | ||
|
|
35fa822c32 | ||
|
|
a842d1b917 | ||
|
|
65a09b2d38 | ||
|
|
469ee5ff85 | ||
|
|
04e6ed30a2 | ||
|
|
ec3d78922e | ||
|
|
bc0ef47323 | ||
|
|
579246dc26 | ||
|
|
4115110c06 | ||
|
|
8e10f522c0 | ||
|
|
d68de99c15 | ||
|
|
fa23a7b8e2 | ||
|
|
57b7f92e61 | ||
|
|
6c3d3aa111 | ||
|
|
dd822c41c5 | ||
|
|
7016985bf2 | ||
|
|
67a6c10edc | ||
|
|
0317dadcaf | ||
|
|
625fddb71e | ||
|
|
63b0ccd035 | ||
|
|
19aa86c027 | ||
|
|
76ad6667f1 | ||
|
|
25c9259b50 | ||
|
|
0e1e855cc5 | ||
|
|
69a847fe8c | ||
|
|
6f2402e16d | ||
|
|
bacd4f385d | ||
|
|
cc42b79fbc | ||
|
|
eaeb503ee7 | ||
|
|
d028932dc8 | ||
|
|
6bdac230df | ||
|
|
43728e451e | ||
|
|
b93b59951b | ||
|
|
b5a8ed229c | ||
|
|
97ae4b6362 | ||
|
|
5a1e53ca7c | ||
|
|
876d383936 | ||
|
|
96196f906f | ||
|
|
0ee9313441 | ||
|
|
496ace8a8e | ||
|
|
0a21c11a35 | ||
|
|
495af733da | ||
|
|
a526869f21 | ||
|
|
789b807542 | ||
|
|
35b3d3931e | ||
|
|
bad4393dda | ||
|
|
6012e8312b | ||
|
|
8f458e55e2 | ||
|
|
61881d99e2 | ||
|
|
3c719f05a1 | ||
|
|
9cba2e509a | ||
|
|
c61eaff525 | ||
|
|
ef0a96182a | ||
|
|
a680f3a9c1 | ||
|
|
ea6a39c6ab | ||
|
|
f0c2860dec | ||
|
|
f9882fe37e | ||
|
|
9c4f8f9e73 | ||
|
|
1a37603e89 | ||
|
|
3e8d2d73d5 | ||
|
|
9900d54f60 | ||
|
|
1321a8bd4d | ||
|
|
85dfabec0a | ||
|
|
15dca79fb7 | ||
|
|
e9b366fa18 | ||
|
|
de246bbff1 | ||
|
|
f20053efe7 | ||
|
|
e404262cb0 | ||
|
|
145dcf4b97 | ||
|
|
52b1dc98b8 | ||
|
|
b32eacc913 | ||
|
|
4a708aa305 | ||
|
|
0bcc8fca5d | ||
|
|
3a1781eb39 | ||
|
|
c90f12208f | ||
|
|
5f328a4c13 | ||
|
|
de11908db1 | ||
|
|
c602314312 | ||
|
|
22044bc474 | ||
|
|
6b03b3cd0a | ||
|
|
59612231bb | ||
|
|
6e9468a56e | ||
|
|
d8dedf8e40 | ||
|
|
8b1f5975d9 | ||
|
|
2fae948edb | ||
|
|
525c4c303f | ||
|
|
81f35ad6aa | ||
|
|
f7a0365bee | ||
|
|
4eae231166 | ||
|
|
ba4540b13e | ||
|
|
01911287f2 | ||
|
|
7b7de2b601 | ||
|
|
b65fccbcf7 | ||
|
|
71c17e1fbb | ||
|
|
296ef20ef7 | ||
|
|
23d6756f03 | ||
|
|
01e6b7fa52 | ||
|
|
348a4d95e9 | ||
|
|
94e166636b | ||
|
|
920dcd105f | ||
|
|
b60e8f0392 | ||
|
|
35d2d8cc01 | ||
|
|
d4b2a3eb27 | ||
|
|
2caa63ae21 | ||
|
|
4c16e5e09c | ||
|
|
ad983c6422 | ||
|
|
0fe6a12d20 | ||
|
|
ce78165b59 | ||
|
|
17c1c733b7 | ||
|
|
3bb9d27dc6 | ||
|
|
04a5ae48e2 | ||
|
|
6d3314f980 | ||
|
|
35541f810d | ||
|
|
3d361028b3 | ||
|
|
7f4b60b8c0 | ||
|
|
1c59eabf5f | ||
|
|
f95282069d | ||
|
|
a3fcf5bda1 | ||
|
|
a7de6406ed | ||
|
|
fd51abb3ce | ||
|
|
cd30306afe | ||
|
|
bed8038d16 | ||
|
|
862a33982d | ||
|
|
90ebb52536 | ||
|
|
072ad72f14 | ||
|
|
387bb15a3d | ||
|
|
077dd31b4f | ||
|
|
99a19cb2a2 | ||
|
|
407cf633e0 | ||
|
|
b0ce01d008 | ||
|
|
3154121840 | ||
|
|
8f2d134d03 | ||
|
|
07bcb6b767 | ||
|
|
8a0226512d | ||
|
|
5418d04529 | ||
|
|
3325b91de9 | ||
|
|
aad5dfc745 | ||
|
|
60d4b5c877 | ||
|
|
9dee9fb366 | ||
|
|
ccc7c6c21d | ||
|
|
896e183e41 | ||
|
|
7c0d70ab3c | ||
|
|
91eeda3a73 | ||
|
|
e4235cbd4b | ||
|
|
fc7f342617 | ||
|
|
6aa9e5fbc9 | ||
|
|
97af998066 | ||
|
|
44e341ab41 | ||
|
|
34c0d39e39 | ||
|
|
686a24d3c6 | ||
|
|
38addacf1e | ||
|
|
a85e1aaa89 | ||
|
|
3307ff8100 | ||
|
|
502043f6de | ||
|
|
dd86e987a4 | ||
|
|
6cd2898923 | ||
|
|
7fec9e7c5c | ||
|
|
2c9a3c5161 | ||
|
|
bb3b1960c5 | ||
|
|
7007a8aa66 | ||
|
|
1ff617703c | ||
|
|
76b7cfec9e | ||
|
|
8d80c73faa | ||
|
|
0461045767 | ||
|
|
e07fba13d8 | ||
|
|
dbc21c8f73 | ||
|
|
7b61a274e5 | ||
|
|
ef8eaa0463 | ||
|
|
65319f93b4 | ||
|
|
dd27c5c4fb | ||
|
|
d1418aa054 | ||
|
|
0c9f05ee38 | ||
|
|
d50b15e639 | ||
|
|
172f1a7a3f | ||
|
|
5edb38691c | ||
|
|
f1f149c6c0 | ||
|
|
e0c5f55fe7 | ||
|
|
4958ee1dda | ||
|
|
3d00f40ea0 | ||
|
|
c9e0957dfe | ||
|
|
9d4f912c93 | ||
|
|
4898a1307e | ||
|
|
6acb751eb3 | ||
|
|
629b7e7433 | ||
|
|
190f18ecae | ||
|
|
e6eb5ad97e | ||
|
|
5f0ecc8dd6 | ||
|
|
e95912f931 | ||
|
|
eb1875f558 | ||
|
|
c761ce8120 | ||
|
|
ee9cb4deec | ||
|
|
a881d175bc | ||
|
|
17ed2be918 | ||
|
|
5a5165818e | ||
|
|
9a7d21438b | ||
|
|
d4d4b8fb3d | ||
|
|
48955e9a71 | ||
|
|
870df88cd1 | ||
|
|
7618a75d85 | ||
|
|
51281095ea | ||
|
|
50a595a8da | ||
|
|
a398367f00 | ||
|
|
fe6faf9aae | ||
|
|
a1331ed514 | ||
|
|
38f2e0beea | ||
|
|
ef4035a462 | ||
|
|
cb07206dae | ||
|
|
cc0405cf27 | ||
|
|
4dd00a98e4 | ||
|
|
b3c321ce02 | ||
|
|
12a796bcbb | ||
|
|
ffcdbf7d75 | ||
|
|
e70c3b7722 | ||
|
|
524a9736b4 | ||
|
|
036a7d9d26 | ||
|
|
c4df2c141a | ||
|
|
c4a2f2c2a8 | ||
|
|
d08be3c7f9 | ||
|
|
7c75c24b5c | ||
|
|
a69611dcb2 | ||
|
|
2588ecaafa | ||
|
|
83af319be3 | ||
|
|
55d7120576 | ||
|
|
73e7a8558d | ||
|
|
a071097c0d | ||
|
|
0b8a79bc25 | ||
|
|
59cb48b7fa | ||
|
|
f45ba5a4f5 | ||
|
|
a0fd19fe17 | ||
|
|
b930091c42 | ||
|
|
4cff240520 | ||
|
|
e40881ed1d | ||
|
|
6a8b2067fd | ||
|
|
3f3f02905f | ||
|
|
edef4c7cee | ||
|
|
53c1a46409 | ||
|
|
0c508ce130 | ||
|
|
3c48b2ceb7 | ||
|
|
64bf02d59c | ||
|
|
a2030d5877 | ||
|
|
f17d062440 | ||
|
|
3a43033fa6 | ||
|
|
9586589453 | ||
|
|
a85dec6dbb | ||
|
|
632d3181f2 | ||
|
|
4e876de458 | ||
|
|
dea504a671 | ||
|
|
877fcb094e | ||
|
|
fc75923211 | ||
|
|
73cab38ba5 | ||
|
|
0cd3275e4a | ||
|
|
9702f142c4 | ||
|
|
2906fec500 | ||
|
|
5e2718f8b2 | ||
|
|
3b0a1a7eb2 | ||
|
|
35cda4eb8c | ||
|
|
fd39f96b4c | ||
|
|
398f7b8fdd | ||
|
|
e2718b37e3 | ||
|
|
95bcd9a7ec | ||
|
|
8d578558ff | ||
|
|
584f5a3426 | ||
|
|
17c69ea1ca | ||
|
|
4ce163691e | ||
|
|
042fc61542 | ||
|
|
7869ec046a | ||
|
|
9beefd1ac3 | ||
|
|
0c59add31f | ||
|
|
26236d3d5b | ||
|
|
43c93fe19a | ||
|
|
7b1b2fa463 | ||
|
|
20cf120b8a | ||
|
|
9ea80123fd | ||
|
|
ee9ccd03d6 | ||
|
|
af7a7ebacc | ||
|
|
7ddd9f8be1 | ||
|
|
a40bb6df24 | ||
|
|
aafd0b3991 | ||
|
|
b641884c37 | ||
|
|
7fac115a36 | ||
|
|
7e8995df24 | ||
|
|
e47b34288b | ||
|
|
6365cc137c | ||
|
|
41ea6f78eb | ||
|
|
9f97426859 | ||
|
|
6e341c1c15 | ||
|
|
b00568176c | ||
|
|
b18672f66d | ||
|
|
887fb93b3b | ||
|
|
d3005393df | ||
|
|
49f04cf403 | ||
|
|
ebaecca949 | ||
|
|
13095a4445 | ||
|
|
b80773b90d | ||
|
|
7dff6ea0ed | ||
|
|
3b39df4b12 | ||
|
|
ab65d46d08 | ||
|
|
077a63b03b | ||
|
|
5be85a45b1 | ||
|
|
6028889909 | ||
|
|
2b5479ae0d | ||
|
|
6a13c8e16e | ||
|
|
89acada310 | ||
|
|
3a2d8d118d | ||
|
|
1b8d23688e | ||
|
|
1209e923fc | ||
|
|
012d1c452b | ||
|
|
ab0487664a | ||
|
|
f504a00ce6 | ||
|
|
f2582c4453 | ||
|
|
820f43078b | ||
|
|
6533a15653 | ||
|
|
7416c8b428 | ||
|
|
8f5e782583 | ||
|
|
39b21830dc | ||
|
|
86cbb2f970 | ||
|
|
0e944e274a | ||
|
|
5e789c2817 | ||
|
|
6150926a75 | ||
|
|
0a2b4287ff | ||
|
|
18ccfa21e0 | ||
|
|
ebc7c9a7a0 | ||
|
|
5bd2b705dc | ||
|
|
2b1a7660b6 | ||
|
|
195b98e688 | ||
|
|
5aedb4fadf | ||
|
|
9cf12b9006 | ||
|
|
86d92e610b | ||
|
|
f2c40ab21a | ||
|
|
0ce6b6d4b1 | ||
|
|
55c49516c8 | ||
|
|
f3c9e828e2 | ||
|
|
3928539ade | ||
|
|
c1386caeb2 | ||
|
|
ade80484bb | ||
|
|
49a5a7448c | ||
|
|
873429db19 | ||
|
|
d6baf4583a | ||
|
|
0bcd52290b | ||
|
|
823e42e635 | ||
|
|
30f4315c17 | ||
|
|
f30240267f | ||
|
|
8cccf74ace | ||
|
|
9b798732b2 | ||
|
|
a7c19f15cd | ||
|
|
493c392422 | ||
|
|
67788bee0b | ||
|
|
0cef537a3d | ||
|
|
46994bea34 | ||
|
|
7ab65b22ec | ||
|
|
9bc245bd40 | ||
|
|
32e2315697 | ||
|
|
3a0a2e3019 | ||
|
|
8ff4b5912a | ||
|
|
7d0656bb14 | ||
|
|
e65c4aead2 | ||
|
|
f43a9288fb | ||
|
|
92e7945329 | ||
|
|
723274523d | ||
|
|
01d78be748 | ||
|
|
bcd87cc7c5 | ||
|
|
c9e7e4f1e0 | ||
|
|
532d03c231 | ||
|
|
f367db741a | ||
|
|
f4f7b4d25b | ||
|
|
63c581577f | ||
|
|
6190bd5f39 | ||
|
|
e29880254e | ||
|
|
ba7904c189 | ||
|
|
46210c5a26 | ||
|
|
ee40f2720a | ||
|
|
f1eba5ea56 | ||
|
|
c76ba691a4 | ||
|
|
ace736c7c2 | ||
|
|
1a78304ca2 | ||
|
|
0c6447a6f5 | ||
|
|
fb87c8bbb9 | ||
|
|
1a4e6ff17b | ||
|
|
3e7695dd2d | ||
|
|
8fcc6cb4db | ||
|
|
dcf19fbd45 | ||
|
|
80ab5ddad2 | ||
|
|
84832a130b | ||
|
|
fcb2e904eb | ||
|
|
36e007e647 | ||
|
|
36b4bd6c5e | ||
|
|
1b676717ea | ||
|
|
4afd360f66 | ||
|
|
dd610b7ed9 | ||
|
|
56ab21558d | ||
|
|
89c53acdcf | ||
|
|
a84f2e5942 | ||
|
|
6cb085f192 | ||
|
|
19fd23c39c | ||
|
|
cf7a737646 | ||
|
|
ff6a5a5565 | ||
|
|
3842eb1328 | ||
|
|
bb5f68c2f0 | ||
|
|
ec7c2892c2 | ||
|
|
5c01706806 | ||
|
|
6c25680115 | ||
|
|
3ca1daf44c | ||
|
|
80cf932ea4 | ||
|
|
abc55cf5e9 | ||
|
|
d4365de4b9 | ||
|
|
5f92af4c0a | ||
|
|
1fc40da052 | ||
|
|
5907cc0c04 | ||
|
|
57588bfc20 | ||
|
|
4afa73521d | ||
|
|
3a69f973d0 | ||
|
|
108d52ce9f | ||
|
|
dd58b70730 | ||
|
|
7ad7b63da2 | ||
|
|
060a789b45 | ||
|
|
bafddd627a | ||
|
|
6f4269aacd | ||
|
|
8b31039557 | ||
|
|
27b80b3e08 | ||
|
|
bdb65f5729 | ||
|
|
f4b95ea5bf | ||
|
|
b149607747 | ||
|
|
9bfcb91774 | ||
|
|
6a8f5c6d9c | ||
|
|
d104a24446 | ||
|
|
a26ef4347a | ||
|
|
e9dba8c9e5 | ||
|
|
2b02db8ae3 | ||
|
|
1ad3b1739b | ||
|
|
9110693c75 | ||
|
|
b8afb6c804 | ||
|
|
37ce09e07c | ||
|
|
334b82bfb4 | ||
|
|
17a2053e79 | ||
|
|
46c3dd252f | ||
|
|
96b0e74794 | ||
|
|
18a2ed2a44 | ||
|
|
7d6ed0cb37 | ||
|
|
396100686c | ||
|
|
35ecb0dd2d | ||
|
|
8d6dae7495 | ||
|
|
340e76c3ed | ||
|
|
275037c73d | ||
|
|
a14ef30c69 | ||
|
|
18fa0f3066 | ||
|
|
3e015591d3 | ||
|
|
81d631ea72 | ||
|
|
2a1ab218ec | ||
|
|
6c31f725ff | ||
|
|
f0bea76141 | ||
|
|
46933a2a81 | ||
|
|
dccb5faa4b | ||
|
|
15ae1fe147 | ||
|
|
6f82f64195 | ||
|
|
1656b4fb7a | ||
|
|
419e954230 | ||
|
|
1a83c9b256 | ||
|
|
a0efa5d351 | ||
|
|
afcda98dc4 | ||
|
|
e508f9c1d1 | ||
|
|
8c2c54b0a4 | ||
|
|
65edddbc36 | ||
|
|
0dfe5a91e7 | ||
|
|
2f75c0bec5 | ||
|
|
516f26edae | ||
|
|
dd8862ce21 | ||
|
|
0c2192d039 | ||
|
|
a85390b289 | ||
|
|
c4a90d7f29 | ||
|
|
8794156f28 | ||
|
|
7603c827f6 | ||
|
|
7ad70a3923 | ||
|
|
95c6a69610 | ||
|
|
157dd71efa | ||
|
|
06ed965a3b | ||
|
|
ea7e273fb4 | ||
|
|
dcf05e4f1c | ||
|
|
50fb7ece4f | ||
|
|
f9db4fffa7 | ||
|
|
1cb6daaa07 | ||
|
|
adf9307796 | ||
|
|
7fdc2b2fab | ||
|
|
0d8043f1f2 | ||
|
|
2c079623a8 | ||
|
|
899c45fc1b | ||
|
|
c763f2a545 | ||
|
|
45dd1d45a1 | ||
|
|
a860b3cf45 | ||
|
|
9fcdd899b2 | ||
|
|
bacafd129f | ||
|
|
20d7fb1949 | ||
|
|
a192eaa20f | ||
|
|
99fe6f6497 | ||
|
|
35c6beca37 | ||
|
|
f7cb92fa9d | ||
|
|
b403d0d570 | ||
|
|
c80ae3367a | ||
|
|
c7312af3c8 | ||
|
|
f3dbc996d4 | ||
|
|
0549b8085a | ||
|
|
760f254f78 | ||
|
|
91bff6c572 | ||
|
|
019ac56ceb | ||
|
|
ecf34b178e | ||
|
|
3cd6e8c13b | ||
|
|
ca8341bf39 | ||
|
|
160bd8bfc7 | ||
|
|
0d1138dfcf | ||
|
|
b112747073 | ||
|
|
e1c3b7528f | ||
|
|
e78bfc80ec | ||
|
|
3eac848d4f | ||
|
|
76cb72812f | ||
|
|
bfc8f9bc26 | ||
|
|
8f2e06bc32 | ||
|
|
2c9f77356f | ||
|
|
ea1b10fea6 | ||
|
|
e6a63ccae1 | ||
|
|
784188cf52 | ||
|
|
1d945f4f75 | ||
|
|
df50cccb7b | ||
|
|
b0a9c89157 | ||
|
|
45eaf91cb3 | ||
|
|
c20b224f30 | ||
|
|
96b941b008 | ||
|
|
ad4da23743 | ||
|
|
5136c32b68 | ||
|
|
cffdec91f1 | ||
|
|
d9c87f8116 | ||
|
|
9954feafd8 | ||
|
|
acfb8d2255 | ||
|
|
fe6106e807 | ||
|
|
2af43d7c2d | ||
|
|
a7a6ff2e6c | ||
|
|
8f598d7ce3 | ||
|
|
7f8092264a | ||
|
|
e0471fef09 | ||
|
|
043edde63b | ||
|
|
4b9a211c49 | ||
|
|
b59bbd93ba | ||
|
|
2a782392bc | ||
|
|
fe56ba133e | ||
|
|
40a3046a3b | ||
|
|
1aa8b5b56b | ||
|
|
266e0c54b9 | ||
|
|
31550ab4e7 | ||
|
|
cce8d1569a | ||
|
|
ad051eb8f0 | ||
|
|
01098545cf | ||
|
|
d58bd782ef | ||
|
|
c11cb6a6cd | ||
|
|
64549f824c | ||
|
|
83fab5321e | ||
|
|
bb47f22d6c | ||
|
|
4996a63bcc | ||
|
|
f302234b0e | ||
|
|
58d6ae02a5 | ||
|
|
f9ec7222f2 | ||
|
|
360b7ebe08 | ||
|
|
ebc99d06eb | ||
|
|
f2600821d6 | ||
|
|
6e08126875 | ||
|
|
176eeca096 | ||
|
|
f6a9ae6335 | ||
|
|
30db67f89c | ||
|
|
da90bafde8 | ||
|
|
04d263b1ed | ||
|
|
e8e79d8446 | ||
|
|
8c24381759 | ||
|
|
8482cdab87 | ||
|
|
064a395c4c | ||
|
|
d103d0aa45 | ||
|
|
9509c8ea00 | ||
|
|
26b73fdaa9 | ||
|
|
a3c9c9cee5 | ||
|
|
0d088962a0 | ||
|
|
2ce4e02ada | ||
|
|
fad3ed1aae | ||
|
|
81444d5603 | ||
|
|
e8b65dbd0b | ||
|
|
f86bb3eab8 | ||
|
|
54a102f029 | ||
|
|
2ee4ec65b4 | ||
|
|
166679cd36 | ||
|
|
b95c54a539 | ||
|
|
e71be53459 | ||
|
|
8c5759d74e | ||
|
|
bd1c4e0690 | ||
|
|
9a428eefe0 | ||
|
|
8774d28bc4 | ||
|
|
9eb9c070cd | ||
|
|
7110a690e1 | ||
|
|
1194e7d51e | ||
|
|
1641f9da5e | ||
|
|
ff4887773e | ||
|
|
15a580ece9 | ||
|
|
b37d258698 | ||
|
|
e0e7bb9190 | ||
|
|
7131c70186 | ||
|
|
be98a59023 | ||
|
|
7e517101a0 | ||
|
|
92f60cceb5 | ||
|
|
b1dcb8a9d7 | ||
|
|
ec6ec7d569 | ||
|
|
31bb069e75 | ||
|
|
363be54303 | ||
|
|
ca0f6661d3 | ||
|
|
cd803cd9bc | ||
|
|
cbdc88c5d0 | ||
|
|
44b548c5c8 | ||
|
|
cc2ac3542d | ||
|
|
25044d40b9 | ||
|
|
676169b189 | ||
|
|
c8c05efb8d | ||
|
|
23cef5fd82 | ||
|
|
c0b0b30541 | ||
|
|
7eeba5f17c | ||
|
|
fcb2457e17 | ||
|
|
02e378905e | ||
|
|
87c0ab6daa | ||
|
|
60f9da9208 | ||
|
|
ff318d6ef5 | ||
|
|
93d9e08de1 | ||
|
|
7edca6b823 | ||
|
|
73eebe7c9e | ||
|
|
049f9a9e37 | ||
|
|
658cbb8bd6 | ||
|
|
19f1c32805 | ||
|
|
ece8ff8cbc | ||
|
|
a3a648aef1 | ||
|
|
3bc2b74d30 | ||
|
|
9d17cd7d9c | ||
|
|
091c6b2737 | ||
|
|
2880314931 | ||
|
|
0102719067 | ||
|
|
123b471b68 | ||
|
|
b66d228460 | ||
|
|
770d67d8c4 | ||
|
|
d42857ec26 | ||
|
|
54b977ee1b | ||
|
|
e8999ba908 | ||
|
|
96c4383b29 | ||
|
|
93d1d2c41a | ||
|
|
b075af5bc9 | ||
|
|
07ca7fccb8 | ||
|
|
797643ffdc | ||
|
|
7d4052be95 | ||
|
|
1036719f2a | ||
|
|
1ab520eda3 | ||
|
|
658f7d816e | ||
|
|
835ab516a6 | ||
|
|
00e098e57d | ||
|
|
f04cac8e2f | ||
|
|
c1b9f1cb28 | ||
|
|
7d8670ff1f | ||
|
|
9f9bcaff65 | ||
|
|
25b1789b0a | ||
|
|
2c8add3b54 | ||
|
|
f25d62fe25 | ||
|
|
7a9f55e1bd | ||
|
|
fbdf1689b3 | ||
|
|
29ba2c5936 | ||
|
|
493050fba1 | ||
|
|
455f6fa95b | ||
|
|
e14957900e | ||
|
|
c0e0f8d214 | ||
|
|
caae869501 | ||
|
|
b998d253bb | ||
|
|
0b1123e3ce | ||
|
|
a412f5d0fb | ||
|
|
919e08689a | ||
|
|
72e803b56d | ||
|
|
e378704c63 | ||
|
|
f6c50ce336 | ||
|
|
063224966c | ||
|
|
5d40e694a5 | ||
|
|
4405a97d9b | ||
|
|
6733de9e0d | ||
|
|
01bae7d43e | ||
|
|
6b30271441 | ||
|
|
cdc8334d82 | ||
|
|
4a3a98b562 | ||
|
|
c280225a4e | ||
|
|
b3ea506a73 | ||
|
|
590437c78b | ||
|
|
a5c61b0546 | ||
|
|
790a1b8e20 | ||
|
|
fa47264c76 | ||
|
|
a4075fb637 | ||
|
|
20a7c8b5a8 | ||
|
|
202494156b | ||
|
|
7558fed4e4 | ||
|
|
480589510e | ||
|
|
999ed5b51b | ||
|
|
589155fa1c | ||
|
|
ae13551033 | ||
|
|
038caeb2a0 | ||
|
|
7b34c9a108 | ||
|
|
5a1fe23ddb | ||
|
|
9bb843f82f | ||
|
|
ebc4f1422a | ||
|
|
96bfa8f131 | ||
|
|
406ba14af5 | ||
|
|
13841b1af6 | ||
|
|
7b3be213e4 | ||
|
|
b52b9ba236 | ||
|
|
58f466b443 | ||
|
|
13e3f05a7a | ||
|
|
7f5cdc0345 | ||
|
|
c21a298e07 | ||
|
|
3d940e21d5 | ||
|
|
6446dd5d3a | ||
|
|
38cff827b3 | ||
|
|
cc4310b368 | ||
|
|
d248e74492 | ||
|
|
a3d74fbe6e | ||
|
|
54dad0135f | ||
|
|
d9440e86a2 | ||
|
|
1f716142af | ||
|
|
f2eb6c3745 | ||
|
|
a08641a59b | ||
|
|
8dc3bdde67 | ||
|
|
223fff9ef9 | ||
|
|
986f6c034f | ||
|
|
10e647570b | ||
|
|
6ac888c5ce | ||
|
|
1bda0259db | ||
|
|
bea115d1e4 | ||
|
|
bc46a18372 | ||
|
|
e2c238f4f8 | ||
|
|
574680fc11 | ||
|
|
673dcd1113 | ||
|
|
41ae35bcdb | ||
|
|
ae02b30aba | ||
|
|
a2ad1d9420 | ||
|
|
1f4e801c58 | ||
|
|
ed65b096e3 | ||
|
|
ff06821fcd | ||
|
|
25edfecbd4 | ||
|
|
aa83583ee9 | ||
|
|
7fe3dff655 | ||
|
|
7c6d9d3723 | ||
|
|
a1ff498585 | ||
|
|
37f45ee89b | ||
|
|
83fbf55781 | ||
|
|
1db24ab887 | ||
|
|
e27e0b2343 | ||
|
|
54311a887c | ||
|
|
89216c01e5 | ||
|
|
9e9cffde6b | ||
|
|
c24cd9721c | ||
|
|
8c33b1c751 | ||
|
|
4d0d15d1d5 | ||
|
|
4ba82e131a | ||
|
|
23ff99d2e2 | ||
|
|
0473b35db3 | ||
|
|
f71533ab17 | ||
|
|
8709b5d34b | ||
|
|
88a059ca52 | ||
|
|
f3ffb22487 | ||
|
|
b915a43eb0 | ||
|
|
8b2b7662ee | ||
|
|
e6d3e8e5a5 | ||
|
|
26e01c930f | ||
|
|
8621a3095d | ||
|
|
6e7352e67e | ||
|
|
7e3f77cb38 | ||
|
|
75b73c55e0 | ||
|
|
ebd928e3b6 | ||
|
|
80cbabeeb0 | ||
|
|
05910905ee | ||
|
|
66fe3392ad | ||
|
|
3a553c892d | ||
|
|
ca506a208e | ||
|
|
cbca6fa6e4 | ||
|
|
951010b64d | ||
|
|
08221c6660 | ||
|
|
ffd8752cde | ||
|
|
deae01712a | ||
|
|
14d1562903 | ||
|
|
8c100230ab | ||
|
|
8eb374d77c | ||
|
|
998ad354d2 | ||
|
|
a2bd1b593b | ||
|
|
2ebb650609 | ||
|
|
11ddcfaf90 | ||
|
|
be4a0b292c | ||
|
|
18494547bc | ||
|
|
26074f9390 | ||
|
|
272905b884 | ||
|
|
0ad2de90ee | ||
|
|
21cbdba530 | ||
|
|
04ccd6f81c | ||
|
|
af04e69dc7 | ||
|
|
25f5f7d6b2 | ||
|
|
2f2eab6e02 | ||
|
|
6726050969 | ||
|
|
d08eba2331 | ||
|
|
7cbdb3db73 | ||
|
|
0519aba820 | ||
|
|
a65b16cbae | ||
|
|
55603cb5c7 |
1
.claude/.gitignore
vendored
Normal file
1
.claude/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hans/
|
||||||
86
.claude/agents/clean-code-architect.md
Normal file
86
.claude/agents/clean-code-architect.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: clean-code-architect
|
||||||
|
description: Use this agent when the user needs help implementing new code or refactoring existing code with a focus on quality, cleanliness, maintainability, and reusability. This includes writing new features, creating utility functions, building modules, or improving existing implementations.\n\nExamples:\n\n<example>\nContext: User requests implementation of a new feature\nuser: "I need a function to validate email addresses"\nassistant: "I'll use the clean-code-architect agent to implement a high-quality, reusable email validation solution."\n<commentary>\nSince the user needs new code implemented, use the clean-code-architect agent to ensure the implementation follows best practices for clean, maintainable, and reusable code.\n</commentary>\n</example>\n\n<example>\nContext: User asks for help building a data processing module\nuser: "Can you help me create a module that handles user authentication?"\nassistant: "Let me engage the clean-code-architect agent to design and implement a robust, maintainable authentication module."\n<commentary>\nThe user needs a new module implemented. The clean-code-architect agent will ensure proper separation of concerns, reusable components, and clean architecture.\n</commentary>\n</example>\n\n<example>\nContext: User wants to implement a utility class\nuser: "I need a helper class for date formatting across my application"\nassistant: "I'll use the clean-code-architect agent to create a well-designed, DRY date formatting utility."\n<commentary>\nThis is a perfect use case for clean-code-architect as utilities need to be highly reusable and maintainable.\n</commentary>\n</example>
|
||||||
|
model: opus
|
||||||
|
color: red
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an elite software architect and clean code craftsman with decades of experience building maintainable, scalable systems. You treat code as a craft, approaching every implementation with the precision of an artist and the rigor of an engineer. Your code has been praised in code reviews across Fortune 500 companies for its clarity, elegance, and robustness.
|
||||||
|
|
||||||
|
## Core Philosophy
|
||||||
|
|
||||||
|
You believe that code is read far more often than it is written. Every line you produce should be immediately understandable to another developer—or to yourself six months from now. You write code that is a joy to maintain and extend.
|
||||||
|
|
||||||
|
## Implementation Principles
|
||||||
|
|
||||||
|
### DRY (Don't Repeat Yourself)
|
||||||
|
|
||||||
|
- Extract common patterns into reusable functions, classes, or modules
|
||||||
|
- Identify repetition not just in code, but in concepts and logic
|
||||||
|
- Create abstractions at the right level—not too early, not too late
|
||||||
|
- Use composition and inheritance judiciously to share behavior
|
||||||
|
- When you see similar code blocks, ask: "What is the underlying abstraction?"
|
||||||
|
|
||||||
|
### Clean Code Standards
|
||||||
|
|
||||||
|
- **Naming**: Use intention-revealing names that make comments unnecessary. Variables should explain what they hold; functions should explain what they do
|
||||||
|
- **Functions**: Keep them small, focused on a single task, and at one level of abstraction. A function should do one thing and do it well
|
||||||
|
- **Classes**: Follow Single Responsibility Principle. A class should have only one reason to change
|
||||||
|
- **Comments**: Write code that doesn't need comments. When comments are necessary, explain "why" not "what"
|
||||||
|
- **Formatting**: Consistent indentation, logical grouping, and visual hierarchy that guides the reader
|
||||||
|
|
||||||
|
### Reusability Architecture
|
||||||
|
|
||||||
|
- Design components with clear interfaces and minimal dependencies
|
||||||
|
- Use dependency injection to decouple implementations from their consumers
|
||||||
|
- Create modules that can be easily extracted and reused in other projects
|
||||||
|
- Follow the Interface Segregation Principle—don't force clients to depend on methods they don't use
|
||||||
|
- Build with configuration over hard-coding; externalize what might change
|
||||||
|
|
||||||
|
### Maintainability Focus
|
||||||
|
|
||||||
|
- Write self-documenting code through expressive naming and clear structure
|
||||||
|
- Keep cognitive complexity low—minimize nested conditionals and loops
|
||||||
|
- Handle errors gracefully with meaningful messages and appropriate recovery
|
||||||
|
- Design for testability from the start; if it's hard to test, it's hard to maintain
|
||||||
|
- Apply the Scout Rule: leave code better than you found it
|
||||||
|
|
||||||
|
## Implementation Process
|
||||||
|
|
||||||
|
1. **Understand Before Building**: Before writing any code, ensure you fully understand the requirements. Ask clarifying questions if the scope is ambiguous.
|
||||||
|
|
||||||
|
2. **Design First**: Consider the architecture before implementation. Think about how this code fits into the larger system, what interfaces it needs, and how it might evolve.
|
||||||
|
|
||||||
|
3. **Implement Incrementally**: Build in small, tested increments. Each piece should work correctly before moving to the next.
|
||||||
|
|
||||||
|
4. **Refactor Continuously**: After getting something working, review it critically. Can it be cleaner? More expressive? More efficient?
|
||||||
|
|
||||||
|
5. **Self-Review**: Before presenting code, review it as if you're seeing it for the first time. Does it make sense? Is anything confusing?
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
|
||||||
|
Before considering any implementation complete, verify:
|
||||||
|
|
||||||
|
- [ ] All names are clear and intention-revealing
|
||||||
|
- [ ] No code duplication exists
|
||||||
|
- [ ] Functions are small and focused
|
||||||
|
- [ ] Error handling is comprehensive and graceful
|
||||||
|
- [ ] The code is testable with clear boundaries
|
||||||
|
- [ ] Dependencies are properly managed and injected
|
||||||
|
- [ ] The code follows established patterns in the codebase
|
||||||
|
- [ ] Edge cases are handled appropriately
|
||||||
|
- [ ] Performance considerations are addressed where relevant
|
||||||
|
|
||||||
|
## Project Context Awareness
|
||||||
|
|
||||||
|
Always consider existing project patterns, coding standards, and architectural decisions from project configuration files. Your implementations should feel native to the codebase, following established conventions while still applying clean code principles.
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- Explain your design decisions and the reasoning behind them
|
||||||
|
- Highlight trade-offs when they exist
|
||||||
|
- Point out where you've applied specific clean code principles
|
||||||
|
- Suggest future improvements or extensions when relevant
|
||||||
|
- If you see opportunities to refactor existing code you encounter, mention them
|
||||||
|
|
||||||
|
You are not just writing code—you are crafting software that will be a pleasure to work with for years to come. Every implementation should be your best work, something you would be proud to show as an example of excellent software engineering.
|
||||||
249
.claude/agents/deepcode.md
Normal file
249
.claude/agents/deepcode.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
---
|
||||||
|
name: deepcode
|
||||||
|
description: >
|
||||||
|
Use this agent to implement, fix, and build code solutions based on AGENT DEEPDIVE's detailed analysis. AGENT DEEPCODE receives findings and recommendations from AGENT DEEPDIVE—who thoroughly investigates bugs, performance issues, security vulnerabilities, and architectural concerns—and is responsible for carrying out the required code changes. Typical workflow:
|
||||||
|
|
||||||
|
- Analyze AGENT DEEPDIVE's handoff, which identifies root causes, file paths, and suggested solutions.
|
||||||
|
- Implement recommended fixes, feature improvements, or refactorings as specified.
|
||||||
|
- Ask for clarification if any aspect of the analysis or requirements is unclear.
|
||||||
|
- Test changes to verify the solution works as intended.
|
||||||
|
- Provide feedback or request further investigation if needed.
|
||||||
|
|
||||||
|
AGENT DEEPCODE should focus on high-quality execution, thorough testing, and clear communication throughout the deep dive/code remediation cycle.
|
||||||
|
model: opus
|
||||||
|
color: yellow
|
||||||
|
---
|
||||||
|
|
||||||
|
# AGENT DEEPCODE
|
||||||
|
|
||||||
|
You are **Agent DEEPCODE**, a coding agent working alongside **Agent DEEPDIVE** (an analysis agent in another Claude instance). The human will copy relevant context between you.
|
||||||
|
|
||||||
|
**Your role:** Implement, fix, and build based on AGENT DEEPDIVE's analysis. You write the code. You can ask AGENT DEEPDIVE for more information when needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 1: GET YOUR BEARINGS (MANDATORY)
|
||||||
|
|
||||||
|
Before ANY work, understand the environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Where are you?
|
||||||
|
pwd
|
||||||
|
|
||||||
|
# 2. What's here?
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
# 3. Understand the project
|
||||||
|
cat README.md 2>/dev/null || echo "No README"
|
||||||
|
find . -type f -name "*.md" | head -20
|
||||||
|
|
||||||
|
# 4. Read any relevant documentation
|
||||||
|
cat *.md 2>/dev/null | head -100
|
||||||
|
cat docs/*.md 2>/dev/null | head -100
|
||||||
|
|
||||||
|
# 5. Understand the tech stack
|
||||||
|
cat package.json 2>/dev/null | head -30
|
||||||
|
cat requirements.txt 2>/dev/null
|
||||||
|
ls src/ 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 2: PARSE AGENT DEEPDIVE'S HANDOFF
|
||||||
|
|
||||||
|
Read AGENT DEEPDIVE's analysis carefully. Extract:
|
||||||
|
|
||||||
|
- **Root cause:** What did they identify as the problem?
|
||||||
|
- **Location:** Which files and line numbers?
|
||||||
|
- **Recommended fix:** What did they suggest?
|
||||||
|
- **Gotchas:** What did they warn you about?
|
||||||
|
- **Verification:** How should you test the fix?
|
||||||
|
|
||||||
|
**If their analysis is unclear or incomplete:**
|
||||||
|
|
||||||
|
- Don't guess — ask AGENT DEEPDIVE for clarification
|
||||||
|
- Be specific about what you need to know
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 3: REVIEW THE CODE
|
||||||
|
|
||||||
|
Before changing anything, read the relevant files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Read files AGENT DEEPDIVE identified
|
||||||
|
cat path/to/file.js
|
||||||
|
cat path/to/other.py
|
||||||
|
|
||||||
|
# Understand the context around the problem area
|
||||||
|
cat -n path/to/file.js | head -100 # With line numbers
|
||||||
|
|
||||||
|
# Check related files they mentioned
|
||||||
|
cat path/to/reference.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify AGENT DEEPDIVE's analysis makes sense.** If something doesn't add up, ask them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 4: IMPLEMENT THE FIX
|
||||||
|
|
||||||
|
Now write the code.
|
||||||
|
|
||||||
|
**Quality standards:**
|
||||||
|
|
||||||
|
- Production-ready code (no lazy shortcuts)
|
||||||
|
- Handle errors properly
|
||||||
|
- Follow existing project patterns and style
|
||||||
|
- No debugging code left behind (console.log, print statements)
|
||||||
|
- Add comments only where logic is non-obvious
|
||||||
|
|
||||||
|
**As you code:**
|
||||||
|
|
||||||
|
- Make targeted changes — don't refactor unrelated code
|
||||||
|
- Keep changes minimal but complete
|
||||||
|
- Handle the edge cases AGENT DEEPDIVE identified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 5: TEST YOUR CHANGES
|
||||||
|
|
||||||
|
**Don't skip this.** Verify your fix actually works.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run existing tests
|
||||||
|
npm test 2>/dev/null
|
||||||
|
pytest 2>/dev/null
|
||||||
|
go test ./... 2>/dev/null
|
||||||
|
|
||||||
|
# Run specific test files if relevant
|
||||||
|
npm test -- --grep "auth"
|
||||||
|
pytest tests/test_auth.py
|
||||||
|
|
||||||
|
# Manual verification (use AGENT DEEPDIVE's "How to Verify" section)
|
||||||
|
curl -s localhost:3000/api/endpoint
|
||||||
|
# [other verification commands]
|
||||||
|
|
||||||
|
# Check for regressions
|
||||||
|
# - Does the original bug still happen? (Should be fixed)
|
||||||
|
# - Did anything else break? (Should still work)
|
||||||
|
```
|
||||||
|
|
||||||
|
**If tests fail, fix them before moving on.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 6: REPORT BACK
|
||||||
|
|
||||||
|
**Always end with a structured response.**
|
||||||
|
|
||||||
|
### If successful:
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## RESPONSE TO AGENT DEEPDIVE
|
||||||
|
|
||||||
|
**Status:** ✅ Implemented and verified
|
||||||
|
|
||||||
|
**What I did:**
|
||||||
|
- [Change 1 with file and brief description]
|
||||||
|
- [Change 2 with file and brief description]
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `path/to/file.js` — [what changed]
|
||||||
|
- `path/to/other.py` — [what changed]
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- [x] Unit tests passing
|
||||||
|
- [x] Manual verification done
|
||||||
|
- [x] Original bug fixed
|
||||||
|
- [x] No regressions found
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- [Anything worth mentioning about the implementation]
|
||||||
|
- [Any deviations from AGENT DEEPDIVE's recommendation and why]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### If you need help from AGENT DEEPDIVE:
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## QUESTION FOR AGENT DEEPDIVE
|
||||||
|
|
||||||
|
**I'm stuck on:** [Specific issue]
|
||||||
|
|
||||||
|
**What I've tried:**
|
||||||
|
- [Attempt 1 and result]
|
||||||
|
- [Attempt 2 and result]
|
||||||
|
|
||||||
|
**What I need from you:**
|
||||||
|
- [Specific question 1]
|
||||||
|
- [Specific question 2]
|
||||||
|
|
||||||
|
**Relevant context:**
|
||||||
|
[Code snippet or error message]
|
||||||
|
|
||||||
|
**My best guess:**
|
||||||
|
[What you think might be the issue, if any]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### If you found issues with the analysis:
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## FEEDBACK FOR AGENT DEEPDIVE
|
||||||
|
|
||||||
|
**Issue with analysis:** [What doesn't match]
|
||||||
|
|
||||||
|
**What I found instead:**
|
||||||
|
- [Your finding]
|
||||||
|
- [Evidence]
|
||||||
|
|
||||||
|
**Questions:**
|
||||||
|
- [What you need clarified]
|
||||||
|
|
||||||
|
**Should I:**
|
||||||
|
- [ ] Wait for your input
|
||||||
|
- [ ] Proceed with my interpretation
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WHEN TO ASK AGENT DEEPDIVE FOR HELP
|
||||||
|
|
||||||
|
Ask AGENT DEEPDIVE when:
|
||||||
|
|
||||||
|
1. **Analysis seems incomplete** — Missing files, unclear root cause
|
||||||
|
2. **You found something different** — Evidence contradicts their findings
|
||||||
|
3. **Multiple valid approaches** — Need guidance on which direction
|
||||||
|
4. **Edge cases unclear** — Not sure how to handle specific scenarios
|
||||||
|
5. **Blocked by missing context** — Need to understand "why" before implementing
|
||||||
|
|
||||||
|
**Be specific when asking:**
|
||||||
|
|
||||||
|
❌ Bad: "I don't understand the auth issue"
|
||||||
|
|
||||||
|
✅ Good: "In src/auth/validate.js, you mentioned line 47, but I see the expiry check on line 52. Also, there's a similar pattern in refresh.js lines 23 AND 45 — should I change both?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RULES
|
||||||
|
|
||||||
|
1. **Understand before coding** — Read AGENT DEEPDIVE's full analysis first
|
||||||
|
2. **Ask if unclear** — Don't guess on important decisions
|
||||||
|
3. **Test your changes** — Verify the fix actually works
|
||||||
|
4. **Stay in scope** — Fix what was identified, flag other issues separately
|
||||||
|
5. **Report back clearly** — AGENT DEEPDIVE should know exactly what you did
|
||||||
|
6. **No half-done work** — Either complete the fix or clearly state what's blocking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REMEMBER
|
||||||
|
|
||||||
|
- AGENT DEEPDIVE did the research — use their findings
|
||||||
|
- You own the implementation — make it production-quality
|
||||||
|
- When in doubt, ask — it's faster than guessing wrong
|
||||||
|
- Test thoroughly — don't assume it works
|
||||||
253
.claude/agents/deepdive.md
Normal file
253
.claude/agents/deepdive.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
name: deepdive
|
||||||
|
description: >
|
||||||
|
Use this agent to investigate, analyze, and uncover root causes for bugs, performance issues, security concerns, and architectural problems. AGENT DEEPDIVE performs deep dives into codebases, reviews files, traces behavior, surfaces vulnerabilities or inefficiencies, and provides detailed findings. Typical workflow:
|
||||||
|
|
||||||
|
- Research and analyze source code, configurations, and project structure.
|
||||||
|
- Identify security vulnerabilities, unusual patterns, logic flaws, or bottlenecks.
|
||||||
|
- Summarize findings with evidence: what, where, and why.
|
||||||
|
- Recommend next diagnostic steps or flag ambiguities for clarification.
|
||||||
|
- Clearly scope the problem—what to fix, relevant files/lines, and testing or verification hints.
|
||||||
|
|
||||||
|
AGENT DEEPDIVE does not write production code or fixes, but arms AGENT DEEPCODE with comprehensive, actionable analysis and context.
|
||||||
|
model: opus
|
||||||
|
color: yellow
|
||||||
|
---
|
||||||
|
|
||||||
|
# AGENT DEEPDIVE - ANALYST
|
||||||
|
|
||||||
|
You are **Agent Deepdive**, an analysis agent working alongside **Agent DEEPCODE** (a coding agent in another Claude instance). The human will copy relevant context between you.
|
||||||
|
|
||||||
|
**Your role:** Research, investigate, analyze, and provide findings. You do NOT write code. You give Agent DEEPCODE the information they need to implement solutions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 1: GET YOUR BEARINGS (MANDATORY)
|
||||||
|
|
||||||
|
Before ANY work, understand the environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Where are you?
|
||||||
|
pwd
|
||||||
|
|
||||||
|
# 2. What's here?
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
# 3. Understand the project
|
||||||
|
cat README.md 2>/dev/null || echo "No README"
|
||||||
|
find . -type f -name "*.md" | head -20
|
||||||
|
|
||||||
|
# 4. Read any relevant documentation
|
||||||
|
cat *.md 2>/dev/null | head -100
|
||||||
|
cat docs/*.md 2>/dev/null | head -100
|
||||||
|
|
||||||
|
# 5. Understand the tech stack
|
||||||
|
cat package.json 2>/dev/null | head -30
|
||||||
|
cat requirements.txt 2>/dev/null
|
||||||
|
ls src/ 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
**Understand the landscape before investigating.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 2: UNDERSTAND THE TASK
|
||||||
|
|
||||||
|
Parse what you're being asked to analyze:
|
||||||
|
|
||||||
|
- **What's the problem?** Bug? Performance issue? Architecture question?
|
||||||
|
- **What's the scope?** Which parts of the system are involved?
|
||||||
|
- **What does success look like?** What does Agent DEEPCODE need from you?
|
||||||
|
- **Is there context from Agent DEEPCODE?** Questions they need answered?
|
||||||
|
|
||||||
|
If unclear, **ask clarifying questions before starting.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 3: INVESTIGATE DEEPLY
|
||||||
|
|
||||||
|
This is your core job. Be thorough.
|
||||||
|
|
||||||
|
**Explore the codebase:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find relevant files
|
||||||
|
find . -type f -name "*.js" | head -20
|
||||||
|
find . -type f -name "*.py" | head -20
|
||||||
|
|
||||||
|
# Search for keywords related to the problem
|
||||||
|
grep -r "error_keyword" --include="*.{js,ts,py}" .
|
||||||
|
grep -r "functionName" --include="*.{js,ts,py}" .
|
||||||
|
grep -r "ClassName" --include="*.{js,ts,py}" .
|
||||||
|
|
||||||
|
# Read relevant files
|
||||||
|
cat src/path/to/relevant-file.js
|
||||||
|
cat src/path/to/another-file.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check logs and errors:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Application logs
|
||||||
|
cat logs/*.log 2>/dev/null | tail -100
|
||||||
|
cat *.log 2>/dev/null | tail -50
|
||||||
|
|
||||||
|
# Look for error patterns
|
||||||
|
grep -r "error\|Error\|ERROR" logs/ 2>/dev/null | tail -30
|
||||||
|
grep -r "exception\|Exception" logs/ 2>/dev/null | tail -30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trace the problem:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Follow the data flow
|
||||||
|
grep -r "functionA" --include="*.{js,ts,py}" . # Where is it defined?
|
||||||
|
grep -r "functionA(" --include="*.{js,ts,py}" . # Where is it called?
|
||||||
|
|
||||||
|
# Check imports/dependencies
|
||||||
|
grep -r "import.*moduleName" --include="*.{js,ts,py}" .
|
||||||
|
grep -r "require.*moduleName" --include="*.{js,ts,py}" .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Document everything you find as you go.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 4: ANALYZE & FORM CONCLUSIONS
|
||||||
|
|
||||||
|
Once you've gathered information:
|
||||||
|
|
||||||
|
1. **Identify the root cause** (or top candidates if uncertain)
|
||||||
|
2. **Trace the chain** — How does the problem manifest?
|
||||||
|
3. **Consider edge cases** — When does it happen? When doesn't it?
|
||||||
|
4. **Evaluate solutions** — What are the options to fix it?
|
||||||
|
5. **Assess risk** — What could go wrong with each approach?
|
||||||
|
|
||||||
|
**Be specific.** Don't say "something's wrong with auth" — say "the token validation in src/auth/validate.js is checking expiry with `<` instead of `<=`, causing tokens to fail 1 second early."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 5: HANDOFF TO Agent DEEPCODE
|
||||||
|
|
||||||
|
**Always end with a structured handoff.** Agent DEEPCODE needs clear, actionable information.
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## HANDOFF TO Agent DEEPCODE
|
||||||
|
|
||||||
|
**Task:** [Original problem/question]
|
||||||
|
|
||||||
|
**Summary:** [1-2 sentence overview of what you found]
|
||||||
|
|
||||||
|
**Root Cause Analysis:**
|
||||||
|
[Detailed explanation of what's causing the problem]
|
||||||
|
|
||||||
|
- **Where:** [File paths and line numbers]
|
||||||
|
- **What:** [Exact issue]
|
||||||
|
- **Why:** [How this causes the observed problem]
|
||||||
|
|
||||||
|
**Evidence:**
|
||||||
|
- [Specific log entry, error message, or code snippet you found]
|
||||||
|
- [Another piece of evidence]
|
||||||
|
- [Pattern you observed]
|
||||||
|
|
||||||
|
**Recommended Fix:**
|
||||||
|
[Describe what needs to change — but don't write the code]
|
||||||
|
|
||||||
|
1. In `path/to/file.js`:
|
||||||
|
- [What needs to change and why]
|
||||||
|
|
||||||
|
2. In `path/to/other.py`:
|
||||||
|
- [What needs to change and why]
|
||||||
|
|
||||||
|
**Alternative Approaches:**
|
||||||
|
1. [Option A] — Pros: [x], Cons: [y]
|
||||||
|
2. [Option B] — Pros: [x], Cons: [y]
|
||||||
|
|
||||||
|
**Things to Watch Out For:**
|
||||||
|
- [Potential gotcha 1]
|
||||||
|
- [Potential gotcha 2]
|
||||||
|
- [Edge case to handle]
|
||||||
|
|
||||||
|
**Files You'll Need to Modify:**
|
||||||
|
- `path/to/file1.js` — [what needs doing]
|
||||||
|
- `path/to/file2.py` — [what needs doing]
|
||||||
|
|
||||||
|
**Files for Reference (don't modify):**
|
||||||
|
- `path/to/reference.js` — [useful pattern here]
|
||||||
|
- `docs/api.md` — [relevant documentation]
|
||||||
|
|
||||||
|
**Open Questions:**
|
||||||
|
- [Anything you're uncertain about]
|
||||||
|
- [Anything that needs more investigation]
|
||||||
|
|
||||||
|
**How to Verify the Fix:**
|
||||||
|
[Describe how Agent DEEPCODE can test that their fix works]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WHEN Agent DEEPCODE ASKS YOU QUESTIONS
|
||||||
|
|
||||||
|
If Agent DEEPCODE sends you questions or needs more analysis:
|
||||||
|
|
||||||
|
1. **Read their full message** — Understand exactly what they're stuck on
|
||||||
|
2. **Investigate further** — Do more targeted research
|
||||||
|
3. **Respond specifically** — Answer their exact questions
|
||||||
|
4. **Provide context** — Give them what they need to proceed
|
||||||
|
|
||||||
|
**Response format:**
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## RESPONSE TO Agent DEEPCODE
|
||||||
|
|
||||||
|
**Regarding:** [Their question/blocker]
|
||||||
|
|
||||||
|
**Answer:**
|
||||||
|
[Direct answer to their question]
|
||||||
|
|
||||||
|
**Additional context:**
|
||||||
|
- [Supporting information]
|
||||||
|
- [Related findings]
|
||||||
|
|
||||||
|
**Files to look at:**
|
||||||
|
- `path/to/file.js` — [relevant section]
|
||||||
|
|
||||||
|
**Suggested approach:**
|
||||||
|
[Your recommendation based on analysis]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RULES
|
||||||
|
|
||||||
|
1. **You do NOT write code** — Describe what needs to change, Agent DEEPCODE implements
|
||||||
|
2. **Be specific** — File paths, line numbers, exact variable names
|
||||||
|
3. **Show your evidence** — Don't just assert, prove it with findings
|
||||||
|
4. **Consider alternatives** — Give Agent DEEPCODE options when possible
|
||||||
|
5. **Flag uncertainty** — If you're not sure, say so
|
||||||
|
6. **Stay focused** — Analyze what was asked, note tangential issues separately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WHAT GOOD ANALYSIS LOOKS LIKE
|
||||||
|
|
||||||
|
**Bad:**
|
||||||
|
|
||||||
|
> "The authentication is broken. Check the auth files."
|
||||||
|
|
||||||
|
**Good:**
|
||||||
|
|
||||||
|
> "The JWT validation fails for tokens expiring within 1 second. In `src/auth/validate.js` line 47, the expiry check uses `token.exp < now` but should use `token.exp <= now`. This causes a race condition where tokens that expire at exactly the current second are incorrectly rejected. You'll need to change the comparison operator. Also check `src/auth/refresh.js` line 23 which has the same pattern."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REMEMBER
|
||||||
|
|
||||||
|
- Your job is to give Agent DEEPCODE everything they need to succeed
|
||||||
|
- Depth over speed — investigate thoroughly
|
||||||
|
- Be the expert who explains the "what" and "why"
|
||||||
|
- Agent DEEPCODE handles the "how" (implementation)
|
||||||
78
.claude/agents/security-vulnerability-scanner.md
Normal file
78
.claude/agents/security-vulnerability-scanner.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: security-vulnerability-scanner
|
||||||
|
description: Use this agent when you need to identify security vulnerabilities in code, perform security audits, or get a prioritized list of security issues to fix. This includes reviewing authentication logic, input validation, data handling, API endpoints, dependency vulnerabilities, and common security anti-patterns.\n\nExamples:\n\n<example>\nContext: User has just written a new authentication endpoint\nuser: "I just finished the login endpoint, can you check it?"\nassistant: "I'll use the security-vulnerability-scanner agent to review your authentication code for potential security issues."\n<Task tool invocation to launch security-vulnerability-scanner agent>\n</example>\n\n<example>\nContext: User wants to review their API before deployment\nuser: "We're about to deploy our API, can you do a security check?"\nassistant: "Let me launch the security-vulnerability-scanner agent to audit your API code for vulnerabilities before deployment."\n<Task tool invocation to launch security-vulnerability-scanner agent>\n</example>\n\n<example>\nContext: User completed a feature involving user data handling\nuser: "Just implemented the user profile update feature"\nassistant: "I'll use the security-vulnerability-scanner agent to check the new code for any security concerns with user data handling."\n<Task tool invocation to launch security-vulnerability-scanner agent>\n</example>
|
||||||
|
model: opus
|
||||||
|
color: yellow
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an elite application security researcher with deep expertise in vulnerability assessment, secure coding practices, and penetration testing. You have extensive experience with OWASP Top 10, CWE classifications, and real-world exploitation techniques. Your mission is to systematically analyze code for security vulnerabilities and deliver a clear, actionable list of issues to fix.
|
||||||
|
|
||||||
|
## Your Approach
|
||||||
|
|
||||||
|
1. **Systematic Analysis**: Methodically examine the code looking for:
|
||||||
|
- Injection vulnerabilities (SQL, NoSQL, Command, LDAP, XPath, etc.)
|
||||||
|
- Authentication and session management flaws
|
||||||
|
- Cross-Site Scripting (XSS) - reflected, stored, and DOM-based
|
||||||
|
- Insecure Direct Object References (IDOR)
|
||||||
|
- Security misconfigurations
|
||||||
|
- Sensitive data exposure
|
||||||
|
- Missing access controls
|
||||||
|
- Cross-Site Request Forgery (CSRF)
|
||||||
|
- Using components with known vulnerabilities
|
||||||
|
- Insufficient logging and monitoring
|
||||||
|
- Race conditions and TOCTOU issues
|
||||||
|
- Cryptographic weaknesses
|
||||||
|
- Path traversal vulnerabilities
|
||||||
|
- Deserialization vulnerabilities
|
||||||
|
- Server-Side Request Forgery (SSRF)
|
||||||
|
|
||||||
|
2. **Context Awareness**: Consider the technology stack, framework conventions, and deployment context when assessing risk.
|
||||||
|
|
||||||
|
3. **Severity Assessment**: Classify each finding by severity (Critical, High, Medium, Low) based on exploitability and potential impact.
|
||||||
|
|
||||||
|
## Research Process
|
||||||
|
|
||||||
|
- Use available tools to read and explore the codebase
|
||||||
|
- Follow data flows from user input to sensitive operations
|
||||||
|
- Check configuration files for security settings
|
||||||
|
- Examine dependency files for known vulnerable packages
|
||||||
|
- Review authentication/authorization logic paths
|
||||||
|
- Analyze error handling and logging practices
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
After your analysis, provide a concise, prioritized list in this format:
|
||||||
|
|
||||||
|
### Security Vulnerabilities Found
|
||||||
|
|
||||||
|
**Critical:**
|
||||||
|
|
||||||
|
- [Brief description] — File: `path/to/file.ext` (line X)
|
||||||
|
|
||||||
|
**High:**
|
||||||
|
|
||||||
|
- [Brief description] — File: `path/to/file.ext` (line X)
|
||||||
|
|
||||||
|
**Medium:**
|
||||||
|
|
||||||
|
- [Brief description] — File: `path/to/file.ext` (line X)
|
||||||
|
|
||||||
|
**Low:**
|
||||||
|
|
||||||
|
- [Brief description] — File: `path/to/file.ext` (line X)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Summary:** X critical, X high, X medium, X low issues found.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Be specific about the vulnerability type and exact location
|
||||||
|
- Keep descriptions concise (one line each)
|
||||||
|
- Only report actual vulnerabilities, not theoretical concerns or style issues
|
||||||
|
- If no vulnerabilities are found in a category, omit that category
|
||||||
|
- If the codebase is clean, clearly state that no significant vulnerabilities were identified
|
||||||
|
- Do not include lengthy explanations or remediation steps in the list (keep it scannable)
|
||||||
|
- Focus on recently modified or newly written code unless explicitly asked to scan the entire codebase
|
||||||
|
|
||||||
|
Your goal is to give the developer a quick, actionable checklist they can work through to improve their application's security posture.
|
||||||
591
.claude/commands/deepreview.md
Normal file
591
.claude/commands/deepreview.md
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
# Code Review Command
|
||||||
|
|
||||||
|
Comprehensive code review using multiple deep dive agents to analyze git diff for correctness, security, code quality, and tech stack compliance, followed by automated fixes using deepcode agents.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This command analyzes all changes in the git diff and verifies:
|
||||||
|
|
||||||
|
1. **Invalid code based on tech stack** (HIGHEST PRIORITY)
|
||||||
|
2. Security vulnerabilities
|
||||||
|
3. Code quality issues (dirty code)
|
||||||
|
4. Implementation correctness
|
||||||
|
|
||||||
|
Then automatically fixes any issues found.
|
||||||
|
|
||||||
|
### Optional Arguments
|
||||||
|
|
||||||
|
- **Target branch**: Optional branch name to compare against (defaults to `main` or `master` if not provided)
|
||||||
|
- Example: `@deepreview develop` - compares current branch against `develop`
|
||||||
|
- If not provided, automatically detects `main` or `master` as the target branch
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Phase 1: Get Git Diff
|
||||||
|
|
||||||
|
1. **Determine the current branch and target branch**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get current branch name
|
||||||
|
CURRENT_BRANCH=$(git branch --show-current)
|
||||||
|
echo "Current branch: $CURRENT_BRANCH"
|
||||||
|
|
||||||
|
# Get target branch from user argument or detect default
|
||||||
|
# If user provided a target branch as argument, use it
|
||||||
|
# Otherwise, detect main or master
|
||||||
|
TARGET_BRANCH="${1:-}" # First argument if provided
|
||||||
|
|
||||||
|
if [ -z "$TARGET_BRANCH" ]; then
|
||||||
|
# Check if main exists
|
||||||
|
if git show-ref --verify --quiet refs/heads/main || git show-ref --verify --quiet refs/remotes/origin/main; then
|
||||||
|
TARGET_BRANCH="main"
|
||||||
|
# Check if master exists
|
||||||
|
elif git show-ref --verify --quiet refs/heads/master || git show-ref --verify --quiet refs/remotes/origin/master; then
|
||||||
|
TARGET_BRANCH="master"
|
||||||
|
else
|
||||||
|
echo "Error: Could not find main or master branch. Please specify target branch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Target branch: $TARGET_BRANCH"
|
||||||
|
|
||||||
|
# Verify target branch exists
|
||||||
|
if ! git show-ref --verify --quiet refs/heads/$TARGET_BRANCH && ! git show-ref --verify --quiet refs/remotes/origin/$TARGET_BRANCH; then
|
||||||
|
echo "Error: Target branch '$TARGET_BRANCH' does not exist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The target branch can be provided as an optional argument. If not provided, the command will automatically detect and use `main` or `master` (in that order).
|
||||||
|
|
||||||
|
2. **Compare current branch against target branch**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fetch latest changes from remote (optional but recommended)
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
# Try local branch first, fallback to remote if local doesn't exist
|
||||||
|
if git show-ref --verify --quiet refs/heads/$TARGET_BRANCH; then
|
||||||
|
TARGET_REF=$TARGET_BRANCH
|
||||||
|
elif git show-ref --verify --quiet refs/remotes/origin/$TARGET_BRANCH; then
|
||||||
|
TARGET_REF=origin/$TARGET_BRANCH
|
||||||
|
else
|
||||||
|
echo "Error: Target branch '$TARGET_BRANCH' not found locally or remotely."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get diff between current branch and target branch
|
||||||
|
git diff $TARGET_REF...HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Use `...` (three dots) to show changes between the common ancestor and HEAD, or `..` (two dots) to show changes between the branches directly. The command uses `$TARGET_BRANCH` variable set in step 1.
|
||||||
|
|
||||||
|
3. **Get list of changed files between branches**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List files changed between current branch and target branch
|
||||||
|
git diff --name-only $TARGET_REF...HEAD
|
||||||
|
|
||||||
|
# Get detailed file status
|
||||||
|
git diff --name-status $TARGET_REF...HEAD
|
||||||
|
|
||||||
|
# Show file changes with statistics
|
||||||
|
git diff --stat $TARGET_REF...HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Get the current working directory diff** (uncommitted changes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uncommitted changes in working directory
|
||||||
|
git diff HEAD
|
||||||
|
|
||||||
|
# Staged changes
|
||||||
|
git diff --cached
|
||||||
|
|
||||||
|
# All changes (staged + unstaged)
|
||||||
|
git diff HEAD
|
||||||
|
git diff --cached
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Combine branch comparison with uncommitted changes**
|
||||||
|
|
||||||
|
The review should analyze:
|
||||||
|
- **Changes between current branch and target branch** (committed changes)
|
||||||
|
- **Uncommitted changes** (if any)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all changes: branch diff + uncommitted
|
||||||
|
git diff $TARGET_REF...HEAD > branch-changes.diff
|
||||||
|
git diff HEAD >> branch-changes.diff
|
||||||
|
git diff --cached >> branch-changes.diff
|
||||||
|
|
||||||
|
# Or get combined diff (recommended approach)
|
||||||
|
git diff $TARGET_REF...HEAD
|
||||||
|
git diff HEAD
|
||||||
|
git diff --cached
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Verify branch relationship**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if current branch is ahead/behind target branch
|
||||||
|
git rev-list --left-right --count $TARGET_REF...HEAD
|
||||||
|
|
||||||
|
# Show commit log differences
|
||||||
|
git log $TARGET_REF..HEAD --oneline
|
||||||
|
|
||||||
|
# Show summary of branch relationship
|
||||||
|
AHEAD=$(git rev-list --left-right --count $TARGET_REF...HEAD | cut -f1)
|
||||||
|
BEHIND=$(git rev-list --left-right --count $TARGET_REF...HEAD | cut -f2)
|
||||||
|
echo "Branch is $AHEAD commits ahead and $BEHIND commits behind $TARGET_BRANCH"
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Understand the tech stack** (for validation):
|
||||||
|
- **Node.js**: >=22.0.0 <23.0.0
|
||||||
|
- **TypeScript**: 5.9.3
|
||||||
|
- **React**: 19.2.3
|
||||||
|
- **Express**: 5.2.1
|
||||||
|
- **Electron**: 39.2.7
|
||||||
|
- **Vite**: 7.3.0
|
||||||
|
- **Vitest**: 4.0.16
|
||||||
|
- Check `package.json` files for exact versions
|
||||||
|
|
||||||
|
### Phase 2: Deep Dive Analysis (5 Agents)
|
||||||
|
|
||||||
|
Launch 5 separate deep dive agents, each with a specific focus area. Each agent should be invoked with the `@deepdive` agent and given the git diff (comparing current branch against target branch) along with their specific instructions.
|
||||||
|
|
||||||
|
**Important:** All agents should analyze the diff between the current branch and target branch (`git diff $TARGET_REF...HEAD`), plus any uncommitted changes. This ensures the review covers all changes that will be merged. The target branch is determined from the optional argument or defaults to main/master.
|
||||||
|
|
||||||
|
#### Agent 1: Tech Stack Validation (HIGHEST PRIORITY)
|
||||||
|
|
||||||
|
**Focus:** Verify code is valid for the tech stack
|
||||||
|
|
||||||
|
**Instructions for Agent 1:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for invalid code based on the tech stack:
|
||||||
|
|
||||||
|
1. **TypeScript/JavaScript Syntax**
|
||||||
|
- Check for valid TypeScript syntax (no invalid type annotations, correct import/export syntax)
|
||||||
|
- Verify Node.js API usage is compatible with Node.js >=22.0.0 <23.0.0
|
||||||
|
- Check for deprecated APIs or features not available in the Node.js version
|
||||||
|
- Verify ES module syntax (type: "module" in package.json)
|
||||||
|
|
||||||
|
2. **React 19.2.3 Compatibility**
|
||||||
|
- Check for deprecated React APIs or patterns
|
||||||
|
- Verify hooks usage is correct for React 19
|
||||||
|
- Check for invalid JSX syntax
|
||||||
|
- Verify component patterns match React 19 conventions
|
||||||
|
|
||||||
|
3. **Express 5.2.1 Compatibility**
|
||||||
|
- Check for deprecated Express APIs
|
||||||
|
- Verify middleware usage is correct for Express 5
|
||||||
|
- Check request/response handling patterns
|
||||||
|
|
||||||
|
4. **Type Safety**
|
||||||
|
- Verify TypeScript types are correctly used
|
||||||
|
- Check for `any` types that should be properly typed
|
||||||
|
- Verify type imports/exports are correct
|
||||||
|
- Check for missing type definitions
|
||||||
|
|
||||||
|
5. **Build System Compatibility**
|
||||||
|
- Verify Vite-specific code (imports, config) is valid
|
||||||
|
- Check Electron-specific APIs are used correctly
|
||||||
|
- Verify module resolution paths are correct
|
||||||
|
|
||||||
|
6. **Package Dependencies**
|
||||||
|
- Check for imports from packages not in package.json
|
||||||
|
- Verify version compatibility between dependencies
|
||||||
|
- Check for circular dependencies
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- File paths and line numbers of invalid code
|
||||||
|
- Specific error description (what's wrong and why)
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Priority level (CRITICAL for build-breaking issues)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 2: Security Vulnerability Scanner
|
||||||
|
|
||||||
|
**Focus:** Security issues and vulnerabilities
|
||||||
|
|
||||||
|
**Instructions for Agent 2:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for security vulnerabilities:
|
||||||
|
|
||||||
|
1. **Injection Vulnerabilities**
|
||||||
|
- SQL injection (if applicable)
|
||||||
|
- Command injection (exec, spawn, etc.)
|
||||||
|
- Path traversal vulnerabilities
|
||||||
|
- XSS vulnerabilities in React components
|
||||||
|
|
||||||
|
2. **Authentication & Authorization**
|
||||||
|
- Missing authentication checks
|
||||||
|
- Insecure token handling
|
||||||
|
- Authorization bypasses
|
||||||
|
- Session management issues
|
||||||
|
|
||||||
|
3. **Data Handling**
|
||||||
|
- Unsafe deserialization
|
||||||
|
- Insecure file operations
|
||||||
|
- Missing input validation
|
||||||
|
- Sensitive data exposure (secrets, tokens, passwords)
|
||||||
|
|
||||||
|
4. **Dependencies**
|
||||||
|
- Known vulnerable packages
|
||||||
|
- Insecure dependency versions
|
||||||
|
- Missing security patches
|
||||||
|
|
||||||
|
5. **API Security**
|
||||||
|
- Missing CORS configuration
|
||||||
|
- Insecure API endpoints
|
||||||
|
- Missing rate limiting
|
||||||
|
- Insecure WebSocket connections
|
||||||
|
|
||||||
|
6. **Electron-Specific**
|
||||||
|
- Insecure IPC communication
|
||||||
|
- Missing context isolation checks
|
||||||
|
- Insecure preload scripts
|
||||||
|
- Missing CSP headers
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Vulnerability type and severity (CRITICAL, HIGH, MEDIUM, LOW)
|
||||||
|
- File paths and line numbers
|
||||||
|
- Attack vector description
|
||||||
|
- Recommended fix approach
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 3: Code Quality & Clean Code
|
||||||
|
|
||||||
|
**Focus:** Dirty code, code smells, and quality issues
|
||||||
|
|
||||||
|
**Instructions for Agent 3:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for code quality issues:
|
||||||
|
|
||||||
|
1. **Code Smells**
|
||||||
|
- Long functions/methods (>50 lines)
|
||||||
|
- High cyclomatic complexity
|
||||||
|
- Duplicate code
|
||||||
|
- Dead code
|
||||||
|
- Magic numbers/strings
|
||||||
|
|
||||||
|
2. **Best Practices**
|
||||||
|
- Missing error handling
|
||||||
|
- Inconsistent naming conventions
|
||||||
|
- Poor separation of concerns
|
||||||
|
- Tight coupling
|
||||||
|
- Missing comments for complex logic
|
||||||
|
|
||||||
|
3. **Performance Issues**
|
||||||
|
- Inefficient algorithms
|
||||||
|
- Memory leaks (event listeners, subscriptions)
|
||||||
|
- Unnecessary re-renders in React
|
||||||
|
- Missing memoization where needed
|
||||||
|
- Inefficient database queries (if applicable)
|
||||||
|
|
||||||
|
4. **Maintainability**
|
||||||
|
- Hard-coded values
|
||||||
|
- Missing type definitions
|
||||||
|
- Inconsistent code style
|
||||||
|
- Poor file organization
|
||||||
|
- Missing tests for new code
|
||||||
|
|
||||||
|
5. **React-Specific**
|
||||||
|
- Missing key props in lists
|
||||||
|
- Direct state mutations
|
||||||
|
- Missing cleanup in useEffect
|
||||||
|
- Unnecessary useState/useEffect
|
||||||
|
- Prop drilling issues
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Issue type and severity
|
||||||
|
- File paths and line numbers
|
||||||
|
- Description of the problem
|
||||||
|
- Impact on maintainability/performance
|
||||||
|
- Recommended refactoring approach
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 4: Implementation Correctness
|
||||||
|
|
||||||
|
**Focus:** Verify code implements requirements correctly
|
||||||
|
|
||||||
|
**Instructions for Agent 4:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for implementation correctness:
|
||||||
|
|
||||||
|
1. **Logic Errors**
|
||||||
|
- Incorrect conditional logic
|
||||||
|
- Wrong variable usage
|
||||||
|
- Off-by-one errors
|
||||||
|
- Race conditions
|
||||||
|
- Missing null/undefined checks
|
||||||
|
|
||||||
|
2. **Functional Requirements**
|
||||||
|
- Missing features from requirements
|
||||||
|
- Incorrect feature implementation
|
||||||
|
- Edge cases not handled
|
||||||
|
- Missing validation
|
||||||
|
|
||||||
|
3. **Integration Issues**
|
||||||
|
- Incorrect API usage
|
||||||
|
- Wrong data format handling
|
||||||
|
- Missing error handling for external calls
|
||||||
|
- Incorrect state management
|
||||||
|
|
||||||
|
4. **Type Errors**
|
||||||
|
- Type mismatches
|
||||||
|
- Missing type guards
|
||||||
|
- Incorrect type assertions
|
||||||
|
- Unsafe type operations
|
||||||
|
|
||||||
|
5. **Testing Gaps**
|
||||||
|
- Missing unit tests
|
||||||
|
- Missing integration tests
|
||||||
|
- Tests don't cover edge cases
|
||||||
|
- Tests are incorrect
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Issue description
|
||||||
|
- File paths and line numbers
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Steps to reproduce (if applicable)
|
||||||
|
- Recommended fix
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 5: Architecture & Design Patterns
|
||||||
|
|
||||||
|
**Focus:** Architectural issues and design pattern violations
|
||||||
|
|
||||||
|
**Instructions for Agent 5:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for architectural and design issues:
|
||||||
|
|
||||||
|
1. **Architecture Violations**
|
||||||
|
- Violation of project structure patterns
|
||||||
|
- Incorrect layer separation
|
||||||
|
- Missing abstractions
|
||||||
|
- Tight coupling between modules
|
||||||
|
|
||||||
|
2. **Design Patterns**
|
||||||
|
- Incorrect pattern usage
|
||||||
|
- Missing patterns where needed
|
||||||
|
- Anti-patterns
|
||||||
|
|
||||||
|
3. **Project-Specific Patterns**
|
||||||
|
- Check against project documentation (docs/ folder)
|
||||||
|
- Verify route organization (server routes)
|
||||||
|
- Check provider patterns (server providers)
|
||||||
|
- Verify component organization (UI components)
|
||||||
|
|
||||||
|
4. **API Design**
|
||||||
|
- RESTful API violations
|
||||||
|
- Inconsistent response formats
|
||||||
|
- Missing error handling
|
||||||
|
- Incorrect status codes
|
||||||
|
|
||||||
|
5. **State Management**
|
||||||
|
- Incorrect state management patterns
|
||||||
|
- Missing state normalization
|
||||||
|
- Inefficient state updates
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Architectural issue description
|
||||||
|
- File paths and affected areas
|
||||||
|
- Impact on system design
|
||||||
|
- Recommended architectural changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Consolidate Findings
|
||||||
|
|
||||||
|
After all 5 deep dive agents complete their analysis:
|
||||||
|
|
||||||
|
1. **Collect all findings** from each agent
|
||||||
|
2. **Prioritize issues**:
|
||||||
|
- CRITICAL: Tech stack invalid code (build-breaking)
|
||||||
|
- HIGH: Security vulnerabilities, critical logic errors
|
||||||
|
- MEDIUM: Code quality issues, architectural problems
|
||||||
|
- LOW: Minor code smells, style issues
|
||||||
|
|
||||||
|
3. **Group by file** to understand impact per file
|
||||||
|
4. **Create a master report** summarizing all findings
|
||||||
|
|
||||||
|
### Phase 4: Deepcode Fixes (5 Agents)
|
||||||
|
|
||||||
|
Launch 5 deepcode agents to fix the issues found. Each agent should be invoked with the `@deepcode` agent.
|
||||||
|
|
||||||
|
#### Deepcode Agent 1: Fix Tech Stack Invalid Code
|
||||||
|
|
||||||
|
**Priority:** CRITICAL - Fix first
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix all invalid code based on tech stack issues identified by Agent 1.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Fixing TypeScript syntax errors
|
||||||
|
2. Updating deprecated Node.js APIs
|
||||||
|
3. Fixing React 19 compatibility issues
|
||||||
|
4. Correcting Express 5 API usage
|
||||||
|
5. Fixing type errors
|
||||||
|
6. Resolving build-breaking issues
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Code compiles without errors
|
||||||
|
- TypeScript types are correct
|
||||||
|
- No deprecated API usage
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 2: Fix Security Vulnerabilities
|
||||||
|
|
||||||
|
**Priority:** HIGH
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix all security vulnerabilities identified by Agent 2.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Adding input validation
|
||||||
|
2. Fixing injection vulnerabilities
|
||||||
|
3. Securing authentication/authorization
|
||||||
|
4. Fixing insecure data handling
|
||||||
|
5. Updating vulnerable dependencies
|
||||||
|
6. Securing Electron IPC
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Security vulnerabilities are addressed
|
||||||
|
- No sensitive data exposure
|
||||||
|
- Proper authentication/authorization
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 3: Refactor Dirty Code
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Refactor code quality issues identified by Agent 3.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Extracting long functions
|
||||||
|
2. Reducing complexity
|
||||||
|
3. Removing duplicate code
|
||||||
|
4. Adding error handling
|
||||||
|
5. Improving React component structure
|
||||||
|
6. Adding missing comments
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Code follows best practices
|
||||||
|
- No code smells remain
|
||||||
|
- Performance optimizations applied
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 4: Fix Implementation Errors
|
||||||
|
|
||||||
|
**Priority:** HIGH
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix implementation correctness issues identified by Agent 4.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Fixing logic errors
|
||||||
|
2. Adding missing features
|
||||||
|
3. Handling edge cases
|
||||||
|
4. Fixing type errors
|
||||||
|
5. Adding missing tests
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Logic is correct
|
||||||
|
- Edge cases handled
|
||||||
|
- Tests pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 5: Fix Architectural Issues
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix architectural issues identified by Agent 5.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Correcting architecture violations
|
||||||
|
2. Applying proper design patterns
|
||||||
|
3. Fixing API design issues
|
||||||
|
4. Improving state management
|
||||||
|
5. Following project patterns
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Architecture is sound
|
||||||
|
- Patterns are correctly applied
|
||||||
|
- Code follows project structure
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Verification
|
||||||
|
|
||||||
|
After all fixes are complete:
|
||||||
|
|
||||||
|
1. **Run TypeScript compilation check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:packages
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run linting**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run tests** (if applicable)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:server
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify git diff** shows only intended changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create summary report**:
|
||||||
|
- Issues found by each agent
|
||||||
|
- Issues fixed by each agent
|
||||||
|
- Remaining issues (if any)
|
||||||
|
- Verification results
|
||||||
|
|
||||||
|
## Workflow Summary
|
||||||
|
|
||||||
|
1. ✅ Accept optional target branch argument (defaults to main/master if not provided)
|
||||||
|
2. ✅ Determine current branch and target branch (from argument or auto-detect main/master)
|
||||||
|
3. ✅ Get git diff comparing current branch against target branch (`git diff $TARGET_REF...HEAD`)
|
||||||
|
4. ✅ Include uncommitted changes in analysis (`git diff HEAD`, `git diff --cached`)
|
||||||
|
5. ✅ Launch 5 deep dive agents (parallel analysis) with branch diff
|
||||||
|
6. ✅ Consolidate findings and prioritize
|
||||||
|
7. ✅ Launch 5 deepcode agents (sequential fixes, priority order)
|
||||||
|
8. ✅ Verify fixes with build/lint/test
|
||||||
|
9. ✅ Report summary
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Tech stack validation is HIGHEST PRIORITY** - invalid code must be fixed first
|
||||||
|
- **Target branch argument**: The command accepts an optional target branch name as the first argument. If not provided, it automatically detects and uses `main` or `master` (in that order)
|
||||||
|
- Each deep dive agent should work independently and provide comprehensive analysis
|
||||||
|
- Deepcode agents should fix issues in priority order
|
||||||
|
- All fixes should maintain existing functionality
|
||||||
|
- If an agent finds no issues in their domain, they should report "No issues found"
|
||||||
|
- If fixes introduce new issues, they should be caught in verification phase
|
||||||
|
- The target branch is validated to ensure it exists (locally or remotely) before proceeding with the review
|
||||||
74
.claude/commands/gh-issue.md
Normal file
74
.claude/commands/gh-issue.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# GitHub Issue Fix Command
|
||||||
|
|
||||||
|
Fetch a GitHub issue by number, verify it's a real issue, and fix it if valid.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This command accepts a GitHub issue number as input (e.g., `123`).
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Get the issue number from the user**
|
||||||
|
- The issue number should be provided as an argument to this command
|
||||||
|
- If no number is provided, ask the user for it
|
||||||
|
|
||||||
|
2. **Fetch the GitHub issue**
|
||||||
|
- Determine the current project path (check if there's a current project context)
|
||||||
|
- Verify the project has a GitHub remote:
|
||||||
|
```bash
|
||||||
|
git remote get-url origin
|
||||||
|
```
|
||||||
|
- Fetch the issue details using GitHub CLI:
|
||||||
|
```bash
|
||||||
|
gh issue view <ISSUE_NUMBER> --json number,title,state,author,createdAt,labels,url,body,assignees
|
||||||
|
```
|
||||||
|
- If the command fails, report the error and stop
|
||||||
|
|
||||||
|
3. **Verify the issue is real and valid**
|
||||||
|
- Check that the issue exists (not 404)
|
||||||
|
- Check the issue state:
|
||||||
|
- If **closed**: Inform the user and ask if they still want to proceed
|
||||||
|
- If **open**: Proceed with validation
|
||||||
|
- Review the issue content:
|
||||||
|
- Read the title and body to understand what needs to be fixed
|
||||||
|
- Check labels for context (bug, enhancement, etc.)
|
||||||
|
- Note any assignees or linked PRs
|
||||||
|
|
||||||
|
4. **Validate the issue**
|
||||||
|
- Determine if this is a legitimate issue that needs fixing:
|
||||||
|
- Is the description clear and actionable?
|
||||||
|
- Does it describe a real problem or feature request?
|
||||||
|
- Are there any obvious signs it's spam or invalid?
|
||||||
|
- If the issue seems invalid or unclear:
|
||||||
|
- Report findings to the user
|
||||||
|
- Ask if they want to proceed anyway
|
||||||
|
- Stop if user confirms it's not valid
|
||||||
|
|
||||||
|
5. **If the issue is valid, proceed to fix it**
|
||||||
|
- Analyze what needs to be done based on the issue description
|
||||||
|
- Check the current codebase state:
|
||||||
|
- Run relevant tests to see current behavior
|
||||||
|
- Check if the issue is already fixed
|
||||||
|
- Look for related code that might need changes
|
||||||
|
- Implement the fix:
|
||||||
|
- Make necessary code changes
|
||||||
|
- Update or add tests as needed
|
||||||
|
- Ensure the fix addresses the issue description
|
||||||
|
- Verify the fix:
|
||||||
|
- Run tests to ensure nothing broke
|
||||||
|
- If possible, manually verify the fix addresses the issue
|
||||||
|
|
||||||
|
6. **Report summary**
|
||||||
|
- Issue number and title
|
||||||
|
- Issue state (open/closed)
|
||||||
|
- Whether the issue was validated as real
|
||||||
|
- What was fixed (if anything)
|
||||||
|
- Any tests that were updated or added
|
||||||
|
- Next steps (if any)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If GitHub CLI (`gh`) is not installed or authenticated, report error and stop
|
||||||
|
- If the project doesn't have a GitHub remote, report error and stop
|
||||||
|
- If the issue number doesn't exist, report error and stop
|
||||||
|
- If the issue is unclear or invalid, report findings and ask user before proceeding
|
||||||
77
.claude/commands/release.md
Normal file
77
.claude/commands/release.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Release Command
|
||||||
|
|
||||||
|
Bump the package.json version (major, minor, or patch) and build the Electron app with the new version.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This command accepts a version bump type as input:
|
||||||
|
|
||||||
|
- `patch` - Bump patch version (0.1.0 -> 0.1.1)
|
||||||
|
- `minor` - Bump minor version (0.1.0 -> 0.2.0)
|
||||||
|
- `major` - Bump major version (0.1.0 -> 1.0.0)
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Get the bump type from the user**
|
||||||
|
- The bump type should be provided as an argument (patch, minor, or major)
|
||||||
|
- If no type is provided, ask the user which type they want
|
||||||
|
|
||||||
|
2. **Bump the version**
|
||||||
|
- Run the version bump script:
|
||||||
|
```bash
|
||||||
|
node apps/ui/scripts/bump-version.mjs <type>
|
||||||
|
```
|
||||||
|
- This updates both `apps/ui/package.json` and `apps/server/package.json` with the new version (keeps them in sync)
|
||||||
|
- Verify the version was updated correctly by checking the output
|
||||||
|
|
||||||
|
3. **Build the Electron app**
|
||||||
|
- Run the electron build:
|
||||||
|
```bash
|
||||||
|
npm run build:electron --workspace=apps/ui
|
||||||
|
```
|
||||||
|
- The build process automatically:
|
||||||
|
- Uses the version from `package.json` for artifact names (e.g., `Automaker-1.2.3-x64.zip`)
|
||||||
|
- Injects the version into the app via Vite's `__APP_VERSION__` constant
|
||||||
|
- Displays the version below the logo in the sidebar
|
||||||
|
|
||||||
|
4. **Commit the version bump**
|
||||||
|
- Stage the updated package.json files:
|
||||||
|
```bash
|
||||||
|
git add apps/ui/package.json apps/server/package.json
|
||||||
|
```
|
||||||
|
- Commit with a release message:
|
||||||
|
```bash
|
||||||
|
git commit -m "chore: release v<version>"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create and push the git tag**
|
||||||
|
- Create an annotated tag for the release:
|
||||||
|
```bash
|
||||||
|
git tag -a v<version> -m "Release v<version>"
|
||||||
|
```
|
||||||
|
- Push the commit and tag to remote:
|
||||||
|
```bash
|
||||||
|
git push && git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Verify the release**
|
||||||
|
- Check that the build completed successfully
|
||||||
|
- Confirm the version appears correctly in the built artifacts
|
||||||
|
- The version will be displayed in the app UI below the logo
|
||||||
|
- Verify the tag is visible on the remote repository
|
||||||
|
|
||||||
|
## Version Centralization
|
||||||
|
|
||||||
|
The version is centralized and synchronized in both `apps/ui/package.json` and `apps/server/package.json`:
|
||||||
|
|
||||||
|
- **Electron builds**: Automatically read from `apps/ui/package.json` via electron-builder's `${version}` variable in `artifactName`
|
||||||
|
- **App display**: Injected at build time via Vite's `define` config as `__APP_VERSION__` constant (defined in `apps/ui/vite.config.mts`)
|
||||||
|
- **Server API**: Read from `apps/server/package.json` via `apps/server/src/lib/version.ts` utility (used in health check endpoints)
|
||||||
|
- **Type safety**: Defined in `apps/ui/src/vite-env.d.ts` as `declare const __APP_VERSION__: string`
|
||||||
|
|
||||||
|
This ensures consistency across:
|
||||||
|
|
||||||
|
- Build artifact names (e.g., `Automaker-1.2.3-x64.zip`)
|
||||||
|
- App UI display (shown as `v1.2.3` below the logo in `apps/ui/src/components/layout/sidebar/components/automaker-logo.tsx`)
|
||||||
|
- Server health endpoints (`/` and `/detailed`)
|
||||||
|
- Package metadata (both UI and server packages stay in sync)
|
||||||
484
.claude/commands/review.md
Normal file
484
.claude/commands/review.md
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
# Code Review Command
|
||||||
|
|
||||||
|
Comprehensive code review using multiple deep dive agents to analyze git diff for correctness, security, code quality, and tech stack compliance, followed by automated fixes using deepcode agents.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This command analyzes all changes in the git diff and verifies:
|
||||||
|
|
||||||
|
1. **Invalid code based on tech stack** (HIGHEST PRIORITY)
|
||||||
|
2. Security vulnerabilities
|
||||||
|
3. Code quality issues (dirty code)
|
||||||
|
4. Implementation correctness
|
||||||
|
|
||||||
|
Then automatically fixes any issues found.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Phase 1: Get Git Diff
|
||||||
|
|
||||||
|
1. **Get the current git diff**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need staged changes instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --cached
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for a specific commit range:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff <base-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Get list of changed files**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --name-only HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Understand the tech stack** (for validation):
|
||||||
|
- **Node.js**: >=22.0.0 <23.0.0
|
||||||
|
- **TypeScript**: 5.9.3
|
||||||
|
- **React**: 19.2.3
|
||||||
|
- **Express**: 5.2.1
|
||||||
|
- **Electron**: 39.2.7
|
||||||
|
- **Vite**: 7.3.0
|
||||||
|
- **Vitest**: 4.0.16
|
||||||
|
- Check `package.json` files for exact versions
|
||||||
|
|
||||||
|
### Phase 2: Deep Dive Analysis (5 Agents)
|
||||||
|
|
||||||
|
Launch 5 separate deep dive agents, each with a specific focus area. Each agent should be invoked with the `@deepdive` agent and given the git diff along with their specific instructions.
|
||||||
|
|
||||||
|
#### Agent 1: Tech Stack Validation (HIGHEST PRIORITY)
|
||||||
|
|
||||||
|
**Focus:** Verify code is valid for the tech stack
|
||||||
|
|
||||||
|
**Instructions for Agent 1:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for invalid code based on the tech stack:
|
||||||
|
|
||||||
|
1. **TypeScript/JavaScript Syntax**
|
||||||
|
- Check for valid TypeScript syntax (no invalid type annotations, correct import/export syntax)
|
||||||
|
- Verify Node.js API usage is compatible with Node.js >=22.0.0 <23.0.0
|
||||||
|
- Check for deprecated APIs or features not available in the Node.js version
|
||||||
|
- Verify ES module syntax (type: "module" in package.json)
|
||||||
|
|
||||||
|
2. **React 19.2.3 Compatibility**
|
||||||
|
- Check for deprecated React APIs or patterns
|
||||||
|
- Verify hooks usage is correct for React 19
|
||||||
|
- Check for invalid JSX syntax
|
||||||
|
- Verify component patterns match React 19 conventions
|
||||||
|
|
||||||
|
3. **Express 5.2.1 Compatibility**
|
||||||
|
- Check for deprecated Express APIs
|
||||||
|
- Verify middleware usage is correct for Express 5
|
||||||
|
- Check request/response handling patterns
|
||||||
|
|
||||||
|
4. **Type Safety**
|
||||||
|
- Verify TypeScript types are correctly used
|
||||||
|
- Check for `any` types that should be properly typed
|
||||||
|
- Verify type imports/exports are correct
|
||||||
|
- Check for missing type definitions
|
||||||
|
|
||||||
|
5. **Build System Compatibility**
|
||||||
|
- Verify Vite-specific code (imports, config) is valid
|
||||||
|
- Check Electron-specific APIs are used correctly
|
||||||
|
- Verify module resolution paths are correct
|
||||||
|
|
||||||
|
6. **Package Dependencies**
|
||||||
|
- Check for imports from packages not in package.json
|
||||||
|
- Verify version compatibility between dependencies
|
||||||
|
- Check for circular dependencies
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- File paths and line numbers of invalid code
|
||||||
|
- Specific error description (what's wrong and why)
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Priority level (CRITICAL for build-breaking issues)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 2: Security Vulnerability Scanner
|
||||||
|
|
||||||
|
**Focus:** Security issues and vulnerabilities
|
||||||
|
|
||||||
|
**Instructions for Agent 2:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for security vulnerabilities:
|
||||||
|
|
||||||
|
1. **Injection Vulnerabilities**
|
||||||
|
- SQL injection (if applicable)
|
||||||
|
- Command injection (exec, spawn, etc.)
|
||||||
|
- Path traversal vulnerabilities
|
||||||
|
- XSS vulnerabilities in React components
|
||||||
|
|
||||||
|
2. **Authentication & Authorization**
|
||||||
|
- Missing authentication checks
|
||||||
|
- Insecure token handling
|
||||||
|
- Authorization bypasses
|
||||||
|
- Session management issues
|
||||||
|
|
||||||
|
3. **Data Handling**
|
||||||
|
- Unsafe deserialization
|
||||||
|
- Insecure file operations
|
||||||
|
- Missing input validation
|
||||||
|
- Sensitive data exposure (secrets, tokens, passwords)
|
||||||
|
|
||||||
|
4. **Dependencies**
|
||||||
|
- Known vulnerable packages
|
||||||
|
- Insecure dependency versions
|
||||||
|
- Missing security patches
|
||||||
|
|
||||||
|
5. **API Security**
|
||||||
|
- Missing CORS configuration
|
||||||
|
- Insecure API endpoints
|
||||||
|
- Missing rate limiting
|
||||||
|
- Insecure WebSocket connections
|
||||||
|
|
||||||
|
6. **Electron-Specific**
|
||||||
|
- Insecure IPC communication
|
||||||
|
- Missing context isolation checks
|
||||||
|
- Insecure preload scripts
|
||||||
|
- Missing CSP headers
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Vulnerability type and severity (CRITICAL, HIGH, MEDIUM, LOW)
|
||||||
|
- File paths and line numbers
|
||||||
|
- Attack vector description
|
||||||
|
- Recommended fix approach
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 3: Code Quality & Clean Code
|
||||||
|
|
||||||
|
**Focus:** Dirty code, code smells, and quality issues
|
||||||
|
|
||||||
|
**Instructions for Agent 3:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for code quality issues:
|
||||||
|
|
||||||
|
1. **Code Smells**
|
||||||
|
- Long functions/methods (>50 lines)
|
||||||
|
- High cyclomatic complexity
|
||||||
|
- Duplicate code
|
||||||
|
- Dead code
|
||||||
|
- Magic numbers/strings
|
||||||
|
|
||||||
|
2. **Best Practices**
|
||||||
|
- Missing error handling
|
||||||
|
- Inconsistent naming conventions
|
||||||
|
- Poor separation of concerns
|
||||||
|
- Tight coupling
|
||||||
|
- Missing comments for complex logic
|
||||||
|
|
||||||
|
3. **Performance Issues**
|
||||||
|
- Inefficient algorithms
|
||||||
|
- Memory leaks (event listeners, subscriptions)
|
||||||
|
- Unnecessary re-renders in React
|
||||||
|
- Missing memoization where needed
|
||||||
|
- Inefficient database queries (if applicable)
|
||||||
|
|
||||||
|
4. **Maintainability**
|
||||||
|
- Hard-coded values
|
||||||
|
- Missing type definitions
|
||||||
|
- Inconsistent code style
|
||||||
|
- Poor file organization
|
||||||
|
- Missing tests for new code
|
||||||
|
|
||||||
|
5. **React-Specific**
|
||||||
|
- Missing key props in lists
|
||||||
|
- Direct state mutations
|
||||||
|
- Missing cleanup in useEffect
|
||||||
|
- Unnecessary useState/useEffect
|
||||||
|
- Prop drilling issues
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Issue type and severity
|
||||||
|
- File paths and line numbers
|
||||||
|
- Description of the problem
|
||||||
|
- Impact on maintainability/performance
|
||||||
|
- Recommended refactoring approach
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 4: Implementation Correctness
|
||||||
|
|
||||||
|
**Focus:** Verify code implements requirements correctly
|
||||||
|
|
||||||
|
**Instructions for Agent 4:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for implementation correctness:
|
||||||
|
|
||||||
|
1. **Logic Errors**
|
||||||
|
- Incorrect conditional logic
|
||||||
|
- Wrong variable usage
|
||||||
|
- Off-by-one errors
|
||||||
|
- Race conditions
|
||||||
|
- Missing null/undefined checks
|
||||||
|
|
||||||
|
2. **Functional Requirements**
|
||||||
|
- Missing features from requirements
|
||||||
|
- Incorrect feature implementation
|
||||||
|
- Edge cases not handled
|
||||||
|
- Missing validation
|
||||||
|
|
||||||
|
3. **Integration Issues**
|
||||||
|
- Incorrect API usage
|
||||||
|
- Wrong data format handling
|
||||||
|
- Missing error handling for external calls
|
||||||
|
- Incorrect state management
|
||||||
|
|
||||||
|
4. **Type Errors**
|
||||||
|
- Type mismatches
|
||||||
|
- Missing type guards
|
||||||
|
- Incorrect type assertions
|
||||||
|
- Unsafe type operations
|
||||||
|
|
||||||
|
5. **Testing Gaps**
|
||||||
|
- Missing unit tests
|
||||||
|
- Missing integration tests
|
||||||
|
- Tests don't cover edge cases
|
||||||
|
- Tests are incorrect
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Issue description
|
||||||
|
- File paths and line numbers
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Steps to reproduce (if applicable)
|
||||||
|
- Recommended fix
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 5: Architecture & Design Patterns
|
||||||
|
|
||||||
|
**Focus:** Architectural issues and design pattern violations
|
||||||
|
|
||||||
|
**Instructions for Agent 5:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyze the git diff for architectural and design issues:
|
||||||
|
|
||||||
|
1. **Architecture Violations**
|
||||||
|
- Violation of project structure patterns
|
||||||
|
- Incorrect layer separation
|
||||||
|
- Missing abstractions
|
||||||
|
- Tight coupling between modules
|
||||||
|
|
||||||
|
2. **Design Patterns**
|
||||||
|
- Incorrect pattern usage
|
||||||
|
- Missing patterns where needed
|
||||||
|
- Anti-patterns
|
||||||
|
|
||||||
|
3. **Project-Specific Patterns**
|
||||||
|
- Check against project documentation (docs/ folder)
|
||||||
|
- Verify route organization (server routes)
|
||||||
|
- Check provider patterns (server providers)
|
||||||
|
- Verify component organization (UI components)
|
||||||
|
|
||||||
|
4. **API Design**
|
||||||
|
- RESTful API violations
|
||||||
|
- Inconsistent response formats
|
||||||
|
- Missing error handling
|
||||||
|
- Incorrect status codes
|
||||||
|
|
||||||
|
5. **State Management**
|
||||||
|
- Incorrect state management patterns
|
||||||
|
- Missing state normalization
|
||||||
|
- Inefficient state updates
|
||||||
|
|
||||||
|
Provide a detailed report with:
|
||||||
|
- Architectural issue description
|
||||||
|
- File paths and affected areas
|
||||||
|
- Impact on system design
|
||||||
|
- Recommended architectural changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Consolidate Findings
|
||||||
|
|
||||||
|
After all 5 deep dive agents complete their analysis:
|
||||||
|
|
||||||
|
1. **Collect all findings** from each agent
|
||||||
|
2. **Prioritize issues**:
|
||||||
|
- CRITICAL: Tech stack invalid code (build-breaking)
|
||||||
|
- HIGH: Security vulnerabilities, critical logic errors
|
||||||
|
- MEDIUM: Code quality issues, architectural problems
|
||||||
|
- LOW: Minor code smells, style issues
|
||||||
|
|
||||||
|
3. **Group by file** to understand impact per file
|
||||||
|
4. **Create a master report** summarizing all findings
|
||||||
|
|
||||||
|
### Phase 4: Deepcode Fixes (5 Agents)
|
||||||
|
|
||||||
|
Launch 5 deepcode agents to fix the issues found. Each agent should be invoked with the `@deepcode` agent.
|
||||||
|
|
||||||
|
#### Deepcode Agent 1: Fix Tech Stack Invalid Code
|
||||||
|
|
||||||
|
**Priority:** CRITICAL - Fix first
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix all invalid code based on tech stack issues identified by Agent 1.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Fixing TypeScript syntax errors
|
||||||
|
2. Updating deprecated Node.js APIs
|
||||||
|
3. Fixing React 19 compatibility issues
|
||||||
|
4. Correcting Express 5 API usage
|
||||||
|
5. Fixing type errors
|
||||||
|
6. Resolving build-breaking issues
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Code compiles without errors
|
||||||
|
- TypeScript types are correct
|
||||||
|
- No deprecated API usage
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 2: Fix Security Vulnerabilities
|
||||||
|
|
||||||
|
**Priority:** HIGH
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix all security vulnerabilities identified by Agent 2.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Adding input validation
|
||||||
|
2. Fixing injection vulnerabilities
|
||||||
|
3. Securing authentication/authorization
|
||||||
|
4. Fixing insecure data handling
|
||||||
|
5. Updating vulnerable dependencies
|
||||||
|
6. Securing Electron IPC
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Security vulnerabilities are addressed
|
||||||
|
- No sensitive data exposure
|
||||||
|
- Proper authentication/authorization
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 3: Refactor Dirty Code
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Refactor code quality issues identified by Agent 3.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Extracting long functions
|
||||||
|
2. Reducing complexity
|
||||||
|
3. Removing duplicate code
|
||||||
|
4. Adding error handling
|
||||||
|
5. Improving React component structure
|
||||||
|
6. Adding missing comments
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Code follows best practices
|
||||||
|
- No code smells remain
|
||||||
|
- Performance optimizations applied
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 4: Fix Implementation Errors
|
||||||
|
|
||||||
|
**Priority:** HIGH
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix implementation correctness issues identified by Agent 4.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Fixing logic errors
|
||||||
|
2. Adding missing features
|
||||||
|
3. Handling edge cases
|
||||||
|
4. Fixing type errors
|
||||||
|
5. Adding missing tests
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Logic is correct
|
||||||
|
- Edge cases handled
|
||||||
|
- Tests pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deepcode Agent 5: Fix Architectural Issues
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix architectural issues identified by Agent 5.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Correcting architecture violations
|
||||||
|
2. Applying proper design patterns
|
||||||
|
3. Fixing API design issues
|
||||||
|
4. Improving state management
|
||||||
|
5. Following project patterns
|
||||||
|
|
||||||
|
After fixes, verify:
|
||||||
|
- Architecture is sound
|
||||||
|
- Patterns are correctly applied
|
||||||
|
- Code follows project structure
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Verification
|
||||||
|
|
||||||
|
After all fixes are complete:
|
||||||
|
|
||||||
|
1. **Run TypeScript compilation check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:packages
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run linting**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run tests** (if applicable)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:server
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify git diff** shows only intended changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create summary report**:
|
||||||
|
- Issues found by each agent
|
||||||
|
- Issues fixed by each agent
|
||||||
|
- Remaining issues (if any)
|
||||||
|
- Verification results
|
||||||
|
|
||||||
|
## Workflow Summary
|
||||||
|
|
||||||
|
1. ✅ Get git diff
|
||||||
|
2. ✅ Launch 5 deep dive agents (parallel analysis)
|
||||||
|
3. ✅ Consolidate findings and prioritize
|
||||||
|
4. ✅ Launch 5 deepcode agents (sequential fixes, priority order)
|
||||||
|
5. ✅ Verify fixes with build/lint/test
|
||||||
|
6. ✅ Report summary
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Tech stack validation is HIGHEST PRIORITY** - invalid code must be fixed first
|
||||||
|
- Each deep dive agent should work independently and provide comprehensive analysis
|
||||||
|
- Deepcode agents should fix issues in priority order
|
||||||
|
- All fixes should maintain existing functionality
|
||||||
|
- If an agent finds no issues in their domain, they should report "No issues found"
|
||||||
|
- If fixes introduce new issues, they should be caught in verification phase
|
||||||
45
.claude/commands/thorough.md
Normal file
45
.claude/commands/thorough.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
When you think you are done, you are NOT done.
|
||||||
|
|
||||||
|
You must run a mandatory 3-pass verification before concluding:
|
||||||
|
|
||||||
|
## Pass 1: Correctness & Functionality
|
||||||
|
|
||||||
|
- [ ] Verify logic matches requirements and specifications
|
||||||
|
- [ ] Check type safety (TypeScript types are correct and complete)
|
||||||
|
- [ ] Ensure imports are correct and follow project conventions
|
||||||
|
- [ ] Verify all functions/classes work as intended
|
||||||
|
- [ ] Check that return values and side effects are correct
|
||||||
|
- [ ] Run relevant tests if they exist, or verify testability
|
||||||
|
- [ ] Confirm integration with existing code works properly
|
||||||
|
|
||||||
|
## Pass 2: Edge Cases & Safety
|
||||||
|
|
||||||
|
- [ ] Handle null/undefined inputs gracefully
|
||||||
|
- [ ] Validate all user inputs and external data
|
||||||
|
- [ ] Check error handling (try/catch, error boundaries, etc.)
|
||||||
|
- [ ] Verify security considerations (no sensitive data exposure, proper auth checks)
|
||||||
|
- [ ] Test boundary conditions (empty arrays, zero values, max lengths, etc.)
|
||||||
|
- [ ] Ensure resource cleanup (file handles, connections, timers)
|
||||||
|
- [ ] Check for potential race conditions or async issues
|
||||||
|
- [ ] Verify file path security (no directory traversal vulnerabilities)
|
||||||
|
|
||||||
|
## Pass 3: Maintainability & Code Quality
|
||||||
|
|
||||||
|
- [ ] Code follows project style guide and conventions
|
||||||
|
- [ ] Functions/classes are single-purpose and well-named
|
||||||
|
- [ ] Remove dead code, unused imports, and console.logs
|
||||||
|
- [ ] Extract magic numbers/strings into named constants
|
||||||
|
- [ ] Check for code duplication (DRY principle)
|
||||||
|
- [ ] Verify appropriate abstraction levels (not over/under-engineered)
|
||||||
|
- [ ] Add necessary comments for complex logic
|
||||||
|
- [ ] Ensure consistent error messages and logging
|
||||||
|
- [ ] Check that code is readable and self-documenting
|
||||||
|
- [ ] Verify proper separation of concerns
|
||||||
|
|
||||||
|
**For each pass, explicitly report:**
|
||||||
|
|
||||||
|
- What you checked
|
||||||
|
- Any issues found and how they were fixed
|
||||||
|
- Any remaining concerns or trade-offs
|
||||||
|
|
||||||
|
Only after completing all three passes with explicit findings may you conclude the work is done.
|
||||||
49
.claude/commands/validate-build.md
Normal file
49
.claude/commands/validate-build.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Project Build and Fix Command
|
||||||
|
|
||||||
|
Run all builds and intelligently fix any failures based on what changed.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Run the build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds all packages and the UI application.
|
||||||
|
|
||||||
|
2. **If the build succeeds**, report success and stop.
|
||||||
|
|
||||||
|
3. **If the build fails**, analyze the failures:
|
||||||
|
- Note which build step failed and the error messages
|
||||||
|
- Check for TypeScript compilation errors, missing dependencies, or configuration issues
|
||||||
|
- Run `git diff main` to see what code has changed
|
||||||
|
|
||||||
|
4. **Determine the nature of the failure**:
|
||||||
|
- **If the failure is due to intentional changes** (new features, refactoring, dependency updates):
|
||||||
|
- Fix any TypeScript type errors introduced by the changes
|
||||||
|
- Update build configuration if needed (e.g., tsconfig.json, vite.config.mts)
|
||||||
|
- Ensure all new dependencies are properly installed
|
||||||
|
- Fix import paths or module resolution issues
|
||||||
|
|
||||||
|
- **If the failure appears to be a regression** (broken imports, missing files, configuration errors):
|
||||||
|
- Fix the source code to restore the build
|
||||||
|
- Check for accidentally deleted files or broken references
|
||||||
|
- Verify build configuration files are correct
|
||||||
|
|
||||||
|
5. **Common build issues to check**:
|
||||||
|
- **TypeScript errors**: Fix type mismatches, missing types, or incorrect imports
|
||||||
|
- **Missing dependencies**: Run `npm install` if packages are missing
|
||||||
|
- **Import/export errors**: Fix incorrect import paths or missing exports
|
||||||
|
- **Build configuration**: Check tsconfig.json, vite.config.mts, or other build configs
|
||||||
|
- **Package build order**: Ensure `build:packages` completes before building apps
|
||||||
|
|
||||||
|
6. **How to decide if it's intentional vs regression**:
|
||||||
|
- Look at the git diff and commit messages
|
||||||
|
- If the change was deliberate and introduced new code that needs fixing → fix the new code
|
||||||
|
- If the change broke existing functionality that should still build → fix the regression
|
||||||
|
- When in doubt, ask the user
|
||||||
|
|
||||||
|
7. **After making fixes**, re-run the build to verify everything compiles successfully.
|
||||||
|
|
||||||
|
8. **Report summary** of what was fixed (TypeScript errors, configuration issues, missing dependencies, etc.).
|
||||||
36
.claude/commands/validate-tests.md
Normal file
36
.claude/commands/validate-tests.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Project Test and Fix Command
|
||||||
|
|
||||||
|
Run all tests and intelligently fix any failures based on what changed.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Run all tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:all
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **If all tests pass**, report success and stop.
|
||||||
|
|
||||||
|
3. **If any tests fail**, analyze the failures:
|
||||||
|
- Note which tests failed and their error messages
|
||||||
|
- Run `git diff main` to see what code has changed
|
||||||
|
|
||||||
|
4. **Determine the nature of the change**:
|
||||||
|
- **If the logic change is intentional** (new feature, refactor, behavior change):
|
||||||
|
- Update the failing tests to match the new expected behavior
|
||||||
|
- The tests should reflect what the code NOW does correctly
|
||||||
|
|
||||||
|
- **If the logic change appears to be a bug** (regression, unintended side effect):
|
||||||
|
- Fix the source code to restore the expected behavior
|
||||||
|
- Do NOT modify the tests - they are catching a real bug
|
||||||
|
|
||||||
|
5. **How to decide if it's a bug vs intentional change**:
|
||||||
|
- Look at the git diff and commit messages
|
||||||
|
- If the change was deliberate and the test expectations are now outdated → update tests
|
||||||
|
- If the change broke existing functionality that should still work → fix the code
|
||||||
|
- When in doubt, ask the user
|
||||||
|
|
||||||
|
6. **After making fixes**, re-run the tests to verify everything passes.
|
||||||
|
|
||||||
|
7. **Report summary** of what was fixed (tests updated vs code fixed).
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"sandbox": {
|
|
||||||
"enabled": true,
|
|
||||||
"autoAllowBashIfSandboxed": true
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"defaultMode": "acceptEdits",
|
|
||||||
"allow": [
|
|
||||||
"Read(./**)",
|
|
||||||
"Write(./**)",
|
|
||||||
"Edit(./**)",
|
|
||||||
"Glob(./**)",
|
|
||||||
"Grep(./**)",
|
|
||||||
"Bash(*)",
|
|
||||||
"mcp__puppeteer__puppeteer_navigate",
|
|
||||||
"mcp__puppeteer__puppeteer_screenshot",
|
|
||||||
"mcp__puppeteer__puppeteer_click",
|
|
||||||
"mcp__puppeteer__puppeteer_fill",
|
|
||||||
"mcp__puppeteer__puppeteer_select",
|
|
||||||
"mcp__puppeteer__puppeteer_hover",
|
|
||||||
"mcp__puppeteer__puppeteer_evaluate"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
117
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
117
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report to help us improve Automaker
|
||||||
|
title: '[Bug]: '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to report a bug! Please fill out the form below with as much detail as possible.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: What operating system are you using?
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- Other
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: run-mode
|
||||||
|
attributes:
|
||||||
|
label: Run Mode
|
||||||
|
description: How are you running Automaker?
|
||||||
|
options:
|
||||||
|
- Electron (Desktop App)
|
||||||
|
- Web (Browser)
|
||||||
|
- Docker
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: App Version
|
||||||
|
description: What version of Automaker are you using? (e.g., 0.1.0)
|
||||||
|
placeholder: '0.1.0'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: Bug Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: Describe the bug...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Steps to reproduce the behavior
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
placeholder: What should have happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: A clear and concise description of what actually happened.
|
||||||
|
placeholder: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: If applicable, add screenshots to help explain your problem.
|
||||||
|
placeholder: Drag and drop screenshots here or paste image URLs
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant Logs
|
||||||
|
description: If applicable, paste relevant logs or error messages.
|
||||||
|
placeholder: Paste logs here...
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
placeholder: Any additional information that might be helpful...
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues to ensure this bug hasn't been reported already
|
||||||
|
required: true
|
||||||
|
- label: I have provided all required information above
|
||||||
|
required: true
|
||||||
71
.github/actions/setup-project/action.yml
vendored
Normal file
71
.github/actions/setup-project/action.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: 'Setup Project'
|
||||||
|
description: 'Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
node-version:
|
||||||
|
description: 'Node.js version to use'
|
||||||
|
required: false
|
||||||
|
default: '22'
|
||||||
|
check-lockfile:
|
||||||
|
description: 'Run lockfile lint check for SSH URLs'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
rebuild-node-pty-path:
|
||||||
|
description: 'Working directory for node-pty rebuild (empty = root)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
|
- name: Check for SSH URLs in lockfile
|
||||||
|
if: inputs.check-lockfile == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: npm run lint:lockfile
|
||||||
|
|
||||||
|
- name: Configure Git for HTTPS
|
||||||
|
shell: bash
|
||||||
|
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
|
||||||
|
# This is needed because SSH authentication isn't available in CI
|
||||||
|
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: bash
|
||||||
|
# Use npm install instead of npm ci to correctly resolve platform-specific
|
||||||
|
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
||||||
|
# Skip scripts to avoid electron-builder install-app-deps which uses too much memory
|
||||||
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
|
- name: Install Linux native bindings
|
||||||
|
shell: bash
|
||||||
|
# Workaround for npm optional dependencies bug (npm/cli#4828)
|
||||||
|
# Explicitly install Linux bindings needed for build tools
|
||||||
|
run: |
|
||||||
|
npm install --no-save --force --ignore-scripts \
|
||||||
|
@rollup/rollup-linux-x64-gnu@4.53.3 \
|
||||||
|
@tailwindcss/oxide-linux-x64-gnu@4.1.17
|
||||||
|
|
||||||
|
- name: Build shared packages
|
||||||
|
shell: bash
|
||||||
|
# Build shared packages (types, utils, platform, etc.) before apps can use them
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Rebuild native modules (root)
|
||||||
|
if: inputs.rebuild-node-pty-path == ''
|
||||||
|
shell: bash
|
||||||
|
# Rebuild node-pty and other native modules for Electron
|
||||||
|
run: npm rebuild node-pty
|
||||||
|
|
||||||
|
- name: Rebuild native modules (workspace)
|
||||||
|
if: inputs.rebuild-node-pty-path != ''
|
||||||
|
shell: bash
|
||||||
|
# Rebuild node-pty and other native modules needed for server
|
||||||
|
run: npm rebuild node-pty
|
||||||
|
working-directory: ${{ inputs.rebuild-node-pty-path }}
|
||||||
283
.github/scripts/upload-to-r2.js
vendored
283
.github/scripts/upload-to-r2.js
vendored
@@ -1,15 +1,11 @@
|
|||||||
const {
|
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
S3Client,
|
const fs = require('fs');
|
||||||
PutObjectCommand,
|
const path = require('path');
|
||||||
GetObjectCommand,
|
const https = require('https');
|
||||||
} = require("@aws-sdk/client-s3");
|
const { pipeline } = require('stream/promises');
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const https = require("https");
|
|
||||||
const { pipeline } = require("stream/promises");
|
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
region: "auto",
|
region: 'auto',
|
||||||
endpoint: process.env.R2_ENDPOINT,
|
endpoint: process.env.R2_ENDPOINT,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||||
@@ -28,14 +24,14 @@ async function fetchExistingReleases() {
|
|||||||
const response = await s3Client.send(
|
const response = await s3Client.send(
|
||||||
new GetObjectCommand({
|
new GetObjectCommand({
|
||||||
Bucket: BUCKET,
|
Bucket: BUCKET,
|
||||||
Key: "releases.json",
|
Key: 'releases.json',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const body = await response.Body.transformToString();
|
const body = await response.Body.transformToString();
|
||||||
return JSON.parse(body);
|
return JSON.parse(body);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
||||||
console.log("No existing releases.json found, creating new one");
|
console.log('No existing releases.json found, creating new one');
|
||||||
return { latestVersion: null, releases: [] };
|
return { latestVersion: null, releases: [] };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -65,39 +61,180 @@ function findArtifacts(dir, pattern) {
|
|||||||
return files.filter((f) => pattern.test(f)).map((f) => path.join(dir, f));
|
return files.filter((f) => pattern.test(f)).map((f) => path.join(dir, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.get(url, { timeout: 10000 }, (response) => {
|
||||||
|
const statusCode = response.statusCode;
|
||||||
|
|
||||||
|
// Follow redirects
|
||||||
|
if (
|
||||||
|
statusCode === 302 ||
|
||||||
|
statusCode === 301 ||
|
||||||
|
statusCode === 307 ||
|
||||||
|
statusCode === 308
|
||||||
|
) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
response.destroy();
|
||||||
|
if (!redirectUrl) {
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode,
|
||||||
|
error: 'Redirect without location header',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Follow the redirect URL
|
||||||
|
return https
|
||||||
|
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
|
||||||
|
const redirectStatus = redirectResponse.statusCode;
|
||||||
|
const contentType = redirectResponse.headers['content-type'] || '';
|
||||||
|
// Check if it's actually a file (zip/tar.gz) and not HTML
|
||||||
|
const isFile =
|
||||||
|
contentType.includes('application/zip') ||
|
||||||
|
contentType.includes('application/gzip') ||
|
||||||
|
contentType.includes('application/x-gzip') ||
|
||||||
|
contentType.includes('application/x-tar') ||
|
||||||
|
redirectUrl.includes('.zip') ||
|
||||||
|
redirectUrl.includes('.tar.gz');
|
||||||
|
const isGood = redirectStatus >= 200 && redirectStatus < 300 && isFile;
|
||||||
|
redirectResponse.destroy();
|
||||||
|
resolve({
|
||||||
|
accessible: isGood,
|
||||||
|
statusCode: redirectStatus,
|
||||||
|
finalUrl: redirectUrl,
|
||||||
|
contentType,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('error', (error) => {
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('timeout', function () {
|
||||||
|
this.destroy();
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode,
|
||||||
|
error: 'Timeout following redirect',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if status is good (200-299 range) and it's actually a file
|
||||||
|
const contentType = response.headers['content-type'] || '';
|
||||||
|
const isFile =
|
||||||
|
contentType.includes('application/zip') ||
|
||||||
|
contentType.includes('application/gzip') ||
|
||||||
|
contentType.includes('application/x-gzip') ||
|
||||||
|
contentType.includes('application/x-tar') ||
|
||||||
|
url.includes('.zip') ||
|
||||||
|
url.includes('.tar.gz');
|
||||||
|
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
|
||||||
|
response.destroy();
|
||||||
|
resolve({ accessible: isGood, statusCode, contentType });
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', (error) => {
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode: null,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('timeout', () => {
|
||||||
|
request.destroy();
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode: null,
|
||||||
|
error: 'Request timeout',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.accessible) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
console.log(
|
||||||
|
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`✓ URL ${url} is accessible (status: ${result.statusCode})`);
|
||||||
|
}
|
||||||
|
return result.finalUrl || url; // Return the final URL (after redirects) if available
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.error ? ` - ${result.error}` : '';
|
||||||
|
const statusMsg = result.statusCode ? ` (status: ${result.statusCode})` : '';
|
||||||
|
const contentTypeMsg = result.contentType ? ` [content-type: ${result.contentType}]` : '';
|
||||||
|
console.log(`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`✗ URL ${url} check failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
const delay = initialDelay * Math.pow(2, attempt);
|
||||||
|
console.log(` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`URL ${url} is not accessible after ${maxRetries} attempts`);
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadFromGitHub(url, outputPath) {
|
async function downloadFromGitHub(url, outputPath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
https
|
const request = https.get(url, { timeout: 30000 }, (response) => {
|
||||||
.get(url, (response) => {
|
const statusCode = response.statusCode;
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
||||||
// Follow redirect
|
// Follow redirects (all redirect types)
|
||||||
return downloadFromGitHub(response.headers.location, outputPath)
|
if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) {
|
||||||
.then(resolve)
|
const redirectUrl = response.headers.location;
|
||||||
.catch(reject);
|
response.destroy();
|
||||||
}
|
if (!redirectUrl) {
|
||||||
if (response.statusCode !== 200) {
|
reject(new Error(`Redirect without location header for ${url}`));
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
`Failed to download ${url}: ${response.statusCode} ${response.statusMessage}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fileStream = fs.createWriteStream(outputPath);
|
// Resolve relative redirects
|
||||||
response.pipe(fileStream);
|
const finalRedirectUrl = redirectUrl.startsWith('http')
|
||||||
fileStream.on("finish", () => {
|
? redirectUrl
|
||||||
fileStream.close();
|
: new URL(redirectUrl, url).href;
|
||||||
resolve();
|
console.log(` Following redirect: ${finalRedirectUrl}`);
|
||||||
});
|
return downloadFromGitHub(finalRedirectUrl, outputPath).then(resolve).catch(reject);
|
||||||
fileStream.on("error", reject);
|
}
|
||||||
})
|
|
||||||
.on("error", reject);
|
if (statusCode !== 200) {
|
||||||
|
response.destroy();
|
||||||
|
reject(new Error(`Failed to download ${url}: ${statusCode} ${response.statusMessage}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = fs.createWriteStream(outputPath);
|
||||||
|
response.pipe(fileStream);
|
||||||
|
fileStream.on('finish', () => {
|
||||||
|
fileStream.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
fileStream.on('error', (error) => {
|
||||||
|
response.destroy();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', reject);
|
||||||
|
request.on('timeout', () => {
|
||||||
|
request.destroy();
|
||||||
|
reject(new Error(`Request timeout for ${url}`));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const artifactsDir = "artifacts";
|
const artifactsDir = 'artifacts';
|
||||||
const tempDir = path.join(artifactsDir, "temp");
|
const tempDir = path.join(artifactsDir, 'temp');
|
||||||
|
|
||||||
// Create temp directory for downloaded GitHub archives
|
// Create temp directory for downloaded GitHub archives
|
||||||
if (!fs.existsSync(tempDir)) {
|
if (!fs.existsSync(tempDir)) {
|
||||||
@@ -111,51 +248,47 @@ async function main() {
|
|||||||
const sourceZipPath = path.join(tempDir, `automaker-${VERSION}.zip`);
|
const sourceZipPath = path.join(tempDir, `automaker-${VERSION}.zip`);
|
||||||
const sourceTarGzPath = path.join(tempDir, `automaker-${VERSION}.tar.gz`);
|
const sourceTarGzPath = path.join(tempDir, `automaker-${VERSION}.tar.gz`);
|
||||||
|
|
||||||
console.log(`Downloading source archives from GitHub...`);
|
console.log(`Waiting for source archives to be available on GitHub...`);
|
||||||
console.log(` ZIP: ${githubZipUrl}`);
|
console.log(` ZIP: ${githubZipUrl}`);
|
||||||
console.log(` TAR.GZ: ${githubTarGzUrl}`);
|
console.log(` TAR.GZ: ${githubTarGzUrl}`);
|
||||||
|
|
||||||
await downloadFromGitHub(githubZipUrl, sourceZipPath);
|
// Wait for archives to be accessible with exponential backoff
|
||||||
await downloadFromGitHub(githubTarGzUrl, sourceTarGzPath);
|
// This returns the final URL after following redirects
|
||||||
|
const finalZipUrl = await checkUrlAccessible(githubZipUrl);
|
||||||
|
const finalTarGzUrl = await checkUrlAccessible(githubTarGzUrl);
|
||||||
|
|
||||||
|
console.log(`Downloading source archives from GitHub...`);
|
||||||
|
await downloadFromGitHub(finalZipUrl, sourceZipPath);
|
||||||
|
await downloadFromGitHub(finalTarGzUrl, sourceTarGzPath);
|
||||||
|
|
||||||
console.log(`Downloaded source archives successfully`);
|
console.log(`Downloaded source archives successfully`);
|
||||||
|
|
||||||
// Find all artifacts
|
// Find all artifacts
|
||||||
const artifacts = {
|
const artifacts = {
|
||||||
windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/),
|
windows: findArtifacts(path.join(artifactsDir, 'windows-builds'), /\.exe$/),
|
||||||
macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/),
|
macos: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-x64\.dmg$/),
|
||||||
macosArm: findArtifacts(
|
macosArm: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-arm64\.dmg$/),
|
||||||
path.join(artifactsDir, "macos-builds"),
|
linux: findArtifacts(path.join(artifactsDir, 'linux-builds'), /\.AppImage$/),
|
||||||
/-arm64\.dmg$/
|
|
||||||
),
|
|
||||||
linux: findArtifacts(
|
|
||||||
path.join(artifactsDir, "linux-builds"),
|
|
||||||
/\.AppImage$/
|
|
||||||
),
|
|
||||||
sourceZip: [sourceZipPath],
|
sourceZip: [sourceZipPath],
|
||||||
sourceTarGz: [sourceTarGzPath],
|
sourceTarGz: [sourceTarGzPath],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Found artifacts:");
|
console.log('Found artifacts:');
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
console.log(
|
console.log(
|
||||||
` ${platform}: ${
|
` ${platform}: ${files.length > 0 ? files.map((f) => path.basename(f)).join(', ') : 'none'}`
|
||||||
files.length > 0
|
|
||||||
? files.map((f) => path.basename(f)).join(", ")
|
|
||||||
: "none"
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload each artifact to R2
|
// Upload each artifact to R2
|
||||||
const assets = {};
|
const assets = {};
|
||||||
const contentTypes = {
|
const contentTypes = {
|
||||||
windows: "application/x-msdownload",
|
windows: 'application/x-msdownload',
|
||||||
macos: "application/x-apple-diskimage",
|
macos: 'application/x-apple-diskimage',
|
||||||
macosArm: "application/x-apple-diskimage",
|
macosArm: 'application/x-apple-diskimage',
|
||||||
linux: "application/x-executable",
|
linux: 'application/x-executable',
|
||||||
sourceZip: "application/zip",
|
sourceZip: 'application/zip',
|
||||||
sourceTarGz: "application/gzip",
|
sourceTarGz: 'application/gzip',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
@@ -175,11 +308,11 @@ async function main() {
|
|||||||
filename,
|
filename,
|
||||||
size,
|
size,
|
||||||
arch:
|
arch:
|
||||||
platform === "macosArm"
|
platform === 'macosArm'
|
||||||
? "arm64"
|
? 'arm64'
|
||||||
: platform === "sourceZip" || platform === "sourceTarGz"
|
: platform === 'sourceZip' || platform === 'sourceTarGz'
|
||||||
? "source"
|
? 'source'
|
||||||
: "x64",
|
: 'x64',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,9 +327,7 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Remove existing entry for this version if re-running
|
// Remove existing entry for this version if re-running
|
||||||
releasesData.releases = releasesData.releases.filter(
|
releasesData.releases = releasesData.releases.filter((r) => r.version !== VERSION);
|
||||||
(r) => r.version !== VERSION
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prepend new release
|
// Prepend new release
|
||||||
releasesData.releases.unshift(newRelease);
|
releasesData.releases.unshift(newRelease);
|
||||||
@@ -206,19 +337,19 @@ async function main() {
|
|||||||
await s3Client.send(
|
await s3Client.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: BUCKET,
|
Bucket: BUCKET,
|
||||||
Key: "releases.json",
|
Key: 'releases.json',
|
||||||
Body: JSON.stringify(releasesData, null, 2),
|
Body: JSON.stringify(releasesData, null, 2),
|
||||||
ContentType: "application/json",
|
ContentType: 'application/json',
|
||||||
CacheControl: "public, max-age=60",
|
CacheControl: 'public, max-age=60',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Successfully updated releases.json");
|
console.log('Successfully updated releases.json');
|
||||||
console.log(`Latest version: ${VERSION}`);
|
console.log(`Latest version: ${VERSION}`);
|
||||||
console.log(`Total releases: ${releasesData.releases.length}`);
|
console.log(`Total releases: ${releasesData.releases.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
console.error("Failed to upload to R2:", err);
|
console.error('Failed to upload to R2:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
49
.github/workflows/claude.yml
vendored
Normal file
49
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Claude Code
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
issues:
|
||||||
|
types: [opened, assigned]
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||||
|
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
|
id-token: write
|
||||||
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Run Claude Code
|
||||||
|
id: claude
|
||||||
|
uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
|
||||||
|
# This is an optional setting that allows Claude to read CI results on PRs
|
||||||
|
additional_permissions: |
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||||
|
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||||
|
|
||||||
|
# Optional: Add claude_args to customize behavior and configuration
|
||||||
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
|
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||||
|
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||||
77
.github/workflows/e2e-tests.yml
vendored
Normal file
77
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup project
|
||||||
|
uses: ./.github/actions/setup-project
|
||||||
|
with:
|
||||||
|
check-lockfile: 'true'
|
||||||
|
rebuild-node-pty-path: 'apps/server'
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
working-directory: apps/ui
|
||||||
|
|
||||||
|
- name: Build server
|
||||||
|
run: npm run build --workspace=apps/server
|
||||||
|
|
||||||
|
- name: Start backend server
|
||||||
|
run: npm run start --workspace=apps/server &
|
||||||
|
env:
|
||||||
|
PORT: 3008
|
||||||
|
NODE_ENV: test
|
||||||
|
|
||||||
|
- name: Wait for backend server
|
||||||
|
run: |
|
||||||
|
echo "Waiting for backend server to be ready..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
|
||||||
|
echo "Backend server is ready!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... ($i/30)"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "Backend server failed to start!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Run E2E tests
|
||||||
|
# Playwright automatically starts the Vite frontend via webServer config
|
||||||
|
# (see apps/ui/playwright.config.ts) - no need to start it manually
|
||||||
|
run: npm run test --workspace=apps/ui
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
VITE_SERVER_URL: http://localhost:3008
|
||||||
|
VITE_SKIP_SETUP: 'true'
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: apps/ui/playwright-report/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: apps/ui/test-results/
|
||||||
|
retention-days: 7
|
||||||
31
.github/workflows/format-check.yml
vendored
Normal file
31
.github/workflows/format-check.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Format Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: npm run format:check
|
||||||
19
.github/workflows/pr-check.yml
vendored
19
.github/workflows/pr-check.yml
vendored
@@ -3,7 +3,7 @@ name: PR Build Check
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- '*'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -17,17 +17,10 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup project
|
||||||
uses: actions/setup-node@v4
|
uses: ./.github/actions/setup-project
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
check-lockfile: 'true'
|
||||||
cache: "npm"
|
|
||||||
cache-dependency-path: package-lock.json
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Run build:electron (dir only - faster CI)
|
||||||
# Use npm install instead of npm ci to correctly resolve platform-specific
|
run: npm run build:electron:dir
|
||||||
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Run build:electron
|
|
||||||
run: npm run build:electron
|
|
||||||
|
|||||||
197
.github/workflows/release.yml
vendored
197
.github/workflows/release.yml
vendored
@@ -1,164 +1,111 @@
|
|||||||
name: Build and Release Electron App
|
name: Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
release:
|
||||||
tags:
|
types: [published]
|
||||||
- "v*.*.*" # Triggers on version tags like v1.0.0
|
|
||||||
workflow_dispatch: # Allows manual triggering
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to release (e.g., v1.0.0)"
|
|
||||||
required: true
|
|
||||||
default: "v0.1.0"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
- os: macos-latest
|
|
||||||
name: macOS
|
|
||||||
artifact-name: macos-builds
|
|
||||||
- os: windows-latest
|
|
||||||
name: Windows
|
|
||||||
artifact-name: windows-builds
|
|
||||||
- os: ubuntu-latest
|
|
||||||
name: Linux
|
|
||||||
artifact-name: linux-builds
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Extract version from tag
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
cache: "npm"
|
|
||||||
cache-dependency-path: package-lock.json
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
# Use npm install instead of npm ci to correctly resolve platform-specific
|
|
||||||
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Extract and set version
|
|
||||||
id: version
|
id: version
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
|
# Remove 'v' prefix if present (e.g., "v1.2.3" -> "1.2.3")
|
||||||
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
|
VERSION="${{ github.event.release.tag_name }}"
|
||||||
VERSION="${VERSION_TAG#v}"
|
VERSION="${VERSION#v}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
|
echo "Extracted version: ${VERSION}"
|
||||||
# Update the app's package.json version
|
|
||||||
cd apps/app
|
|
||||||
npm version $VERSION --no-git-tag-version
|
|
||||||
cd ../..
|
|
||||||
echo "Updated apps/app/package.json to version $VERSION"
|
|
||||||
|
|
||||||
- name: Build Electron App (macOS)
|
- name: Update package.json version
|
||||||
if: matrix.os == 'macos-latest'
|
shell: bash
|
||||||
env:
|
run: |
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
node apps/ui/scripts/update-version.mjs "${{ steps.version.outputs.version }}"
|
||||||
run: npm run build:electron -- --mac --x64 --arm64
|
|
||||||
|
|
||||||
- name: Build Electron App (Windows)
|
- name: Setup project
|
||||||
if: matrix.os == 'windows-latest'
|
uses: ./.github/actions/setup-project
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: npm run build:electron -- --win --x64
|
|
||||||
|
|
||||||
- name: Build Electron App (Linux)
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: npm run build:electron -- --linux --x64
|
|
||||||
|
|
||||||
- name: Upload Release Assets
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.event.inputs.version || github.ref_name }}
|
check-lockfile: 'true'
|
||||||
files: |
|
|
||||||
apps/app/dist/*.exe
|
|
||||||
apps/app/dist/*.dmg
|
|
||||||
apps/app/dist/*.AppImage
|
|
||||||
apps/app/dist/*.zip
|
|
||||||
apps/app/dist/*.deb
|
|
||||||
apps/app/dist/*.rpm
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload macOS artifacts for R2
|
- name: Build Electron app (macOS)
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
shell: bash
|
||||||
|
run: npm run build:electron:mac --workspace=apps/ui
|
||||||
|
env:
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
|
|
||||||
|
- name: Build Electron app (Windows)
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
shell: bash
|
||||||
|
run: npm run build:electron:win --workspace=apps/ui
|
||||||
|
|
||||||
|
- name: Build Electron app (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
shell: bash
|
||||||
|
run: npm run build:electron:linux --workspace=apps/ui
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact-name }}
|
name: macos-builds
|
||||||
path: apps/app/dist/*.dmg
|
path: apps/ui/release/*.{dmg,zip}
|
||||||
retention-days: 1
|
retention-days: 30
|
||||||
|
|
||||||
- name: Upload Windows artifacts for R2
|
- name: Upload Windows artifacts
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact-name }}
|
name: windows-builds
|
||||||
path: apps/app/dist/*.exe
|
path: apps/ui/release/*.exe
|
||||||
retention-days: 1
|
retention-days: 30
|
||||||
|
|
||||||
- name: Upload Linux artifacts for R2
|
- name: Upload Linux artifacts
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact-name }}
|
name: linux-builds
|
||||||
path: apps/app/dist/*.AppImage
|
path: apps/ui/release/*.{AppImage,deb}
|
||||||
retention-days: 1
|
retention-days: 30
|
||||||
|
|
||||||
upload-to-r2:
|
upload:
|
||||||
needs: build-and-release
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.release.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Download macOS artifacts
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
name: macos-builds
|
||||||
|
path: artifacts/macos-builds
|
||||||
|
|
||||||
- name: Install AWS SDK
|
- name: Download Windows artifacts
|
||||||
run: npm install @aws-sdk/client-s3
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-builds
|
||||||
|
path: artifacts/windows-builds
|
||||||
|
|
||||||
- name: Extract version
|
- name: Download Linux artifacts
|
||||||
id: version
|
uses: actions/download-artifact@v4
|
||||||
run: |
|
with:
|
||||||
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
|
name: linux-builds
|
||||||
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
|
path: artifacts/linux-builds
|
||||||
VERSION="${VERSION_TAG#v}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
|
|
||||||
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
|
|
||||||
|
|
||||||
- name: Upload to R2 and update releases.json
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
artifacts/macos-builds/*
|
||||||
|
artifacts/windows-builds/*
|
||||||
|
artifacts/linux-builds/*
|
||||||
env:
|
env:
|
||||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
||||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
||||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
|
||||||
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
|
||||||
RELEASE_VERSION: ${{ steps.version.outputs.version }}
|
|
||||||
RELEASE_TAG: ${{ steps.version.outputs.version_tag }}
|
|
||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
||||||
run: node .github/scripts/upload-to-r2.js
|
|
||||||
|
|||||||
30
.github/workflows/security-audit.yml
vendored
Normal file
30
.github/workflows/security-audit.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Mondays at 9 AM UTC
|
||||||
|
- cron: '0 9 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup project
|
||||||
|
uses: ./.github/actions/setup-project
|
||||||
|
with:
|
||||||
|
check-lockfile: 'true'
|
||||||
|
|
||||||
|
- name: Run npm audit
|
||||||
|
run: npm audit --audit-level=moderate
|
||||||
|
continue-on-error: false
|
||||||
44
.github/workflows/test.yml
vendored
Normal file
44
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Test Suite
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup project
|
||||||
|
uses: ./.github/actions/setup-project
|
||||||
|
with:
|
||||||
|
check-lockfile: 'true'
|
||||||
|
rebuild-node-pty-path: 'apps/server'
|
||||||
|
|
||||||
|
- name: Run package tests
|
||||||
|
run: npm run test:packages
|
||||||
|
env:
|
||||||
|
NODE_ENV: test
|
||||||
|
|
||||||
|
- name: Run server tests with coverage
|
||||||
|
run: npm run test:server:coverage
|
||||||
|
env:
|
||||||
|
NODE_ENV: test
|
||||||
|
|
||||||
|
# - name: Upload coverage reports
|
||||||
|
# uses: codecov/codecov-action@v4
|
||||||
|
# if: always()
|
||||||
|
# with:
|
||||||
|
# files: ./apps/server/coverage/coverage-final.json
|
||||||
|
# flags: server
|
||||||
|
# name: server-coverage
|
||||||
|
# env:
|
||||||
|
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
74
.gitignore
vendored
74
.gitignore
vendored
@@ -6,8 +6,82 @@ node_modules/
|
|||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
.next/
|
.next/
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
# Automaker
|
||||||
.automaker/images/
|
.automaker/images/
|
||||||
.automaker/
|
.automaker/
|
||||||
/.automaker/*
|
/.automaker/*
|
||||||
/.automaker/
|
/.automaker/
|
||||||
|
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
|
/logs
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# OS-specific files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# IDE/Editor configs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# Editor backup/temp files
|
||||||
|
*~
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.orig
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Local settings (user-specific)
|
||||||
|
*.local.json
|
||||||
|
|
||||||
|
# Application state/backup
|
||||||
|
backup.json
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
test-results/
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.lcov
|
||||||
|
playwright-report/
|
||||||
|
blob-report/
|
||||||
|
|
||||||
|
# Environment files (keep .example)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
!.env.example
|
||||||
|
!.env.local.example
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
docker-compose.override.yml
|
||||||
|
.claude/docker-compose.override.yml
|
||||||
|
.claude/hans/
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
46
.husky/pre-commit
Executable file
46
.husky/pre-commit
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Try to load nvm if available (optional - works without it too)
|
||||||
|
if [ -z "$NVM_DIR" ]; then
|
||||||
|
# Check for Herd's nvm first (macOS with Herd)
|
||||||
|
if [ -s "$HOME/Library/Application Support/Herd/config/nvm/nvm.sh" ]; then
|
||||||
|
export NVM_DIR="$HOME/Library/Application Support/Herd/config/nvm"
|
||||||
|
# Then check standard nvm location
|
||||||
|
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source nvm if found (silently skip if not available)
|
||||||
|
[ -n "$NVM_DIR" ] && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 2>/dev/null
|
||||||
|
|
||||||
|
# Load node version from .nvmrc if using nvm (silently skip if nvm not available or fails)
|
||||||
|
if [ -f .nvmrc ] && command -v nvm >/dev/null 2>&1; then
|
||||||
|
# Check if Unix nvm was sourced (it's a shell function with NVM_DIR set)
|
||||||
|
if [ -n "$NVM_DIR" ] && type nvm 2>/dev/null | grep -q "function"; then
|
||||||
|
# Unix nvm: reads .nvmrc automatically
|
||||||
|
nvm use >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
# nvm-windows: needs explicit version from .nvmrc
|
||||||
|
NODE_VERSION=$(cat .nvmrc | tr -d '[:space:]')
|
||||||
|
if [ -n "$NODE_VERSION" ]; then
|
||||||
|
nvm use "$NODE_VERSION" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure common system paths are in PATH (for systems without nvm)
|
||||||
|
# This helps find node/npm installed via Homebrew, system packages, etc.
|
||||||
|
export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin"
|
||||||
|
|
||||||
|
# Run lint-staged - works with or without nvm
|
||||||
|
# Prefer npx, fallback to npm exec, both work with system-installed Node.js
|
||||||
|
if command -v npx >/dev/null 2>&1; then
|
||||||
|
npx lint-staged
|
||||||
|
elif command -v npm >/dev/null 2>&1; then
|
||||||
|
npm exec -- lint-staged
|
||||||
|
else
|
||||||
|
echo "Error: Neither npx nor npm found in PATH."
|
||||||
|
echo "Please ensure Node.js is installed (via nvm, Homebrew, system package manager, etc.)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
6
.npmrc
6
.npmrc
@@ -8,3 +8,9 @@
|
|||||||
#
|
#
|
||||||
# In CI/CD: Use "npm install" instead of "npm ci" to allow npm to resolve
|
# In CI/CD: Use "npm install" instead of "npm ci" to allow npm to resolve
|
||||||
# the correct platform-specific binaries at install time.
|
# the correct platform-specific binaries at install time.
|
||||||
|
|
||||||
|
# Include bindings for all platforms in package-lock.json to support CI/CD
|
||||||
|
# This ensures Linux, macOS, and Windows bindings are all present
|
||||||
|
# NOTE: Only enable when regenerating package-lock.json, then comment out to keep installs fast
|
||||||
|
# supportedArchitectures.os=linux,darwin,win32
|
||||||
|
# supportedArchitectures.cpu=x64,arm64
|
||||||
|
|||||||
41
.prettierignore
Normal file
41
.prettierignore
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
.next/
|
||||||
|
.turbo/
|
||||||
|
release/
|
||||||
|
|
||||||
|
# Automaker
|
||||||
|
.automaker/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
routeTree.gen.ts
|
||||||
|
apps/ui/src/routeTree.gen.ts
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
test-results/
|
||||||
|
coverage/
|
||||||
|
playwright-report/
|
||||||
|
blob-report/
|
||||||
|
|
||||||
|
# IDE/Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Electron
|
||||||
|
dist-electron/
|
||||||
|
server-bundle/
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
213
CLAUDE.md
Normal file
213
CLAUDE.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Automaker is an autonomous AI development studio built as an npm workspace monorepo. It provides a Kanban-based workflow where AI agents (powered by Claude Agent SDK) implement features in isolated git worktrees.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Interactive launcher (choose web or electron)
|
||||||
|
npm run dev:web # Web browser mode (localhost:3007)
|
||||||
|
npm run dev:electron # Desktop app mode
|
||||||
|
npm run dev:electron:debug # Desktop with DevTools open
|
||||||
|
|
||||||
|
# Building
|
||||||
|
npm run build # Build web application
|
||||||
|
npm run build:packages # Build all shared packages (required before other builds)
|
||||||
|
npm run build:electron # Build desktop app for current platform
|
||||||
|
npm run build:server # Build server only
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npm run test # E2E tests (Playwright, headless)
|
||||||
|
npm run test:headed # E2E tests with browser visible
|
||||||
|
npm run test:server # Server unit tests (Vitest)
|
||||||
|
npm run test:packages # All shared package tests
|
||||||
|
npm run test:all # All tests (packages + server)
|
||||||
|
|
||||||
|
# Single test file
|
||||||
|
npm run test:server -- tests/unit/specific.test.ts
|
||||||
|
|
||||||
|
# Linting and formatting
|
||||||
|
npm run lint # ESLint
|
||||||
|
npm run format # Prettier write
|
||||||
|
npm run format:check # Prettier check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
automaker/
|
||||||
|
├── apps/
|
||||||
|
│ ├── ui/ # React + Vite + Electron frontend (port 3007)
|
||||||
|
│ └── server/ # Express + WebSocket backend (port 3008)
|
||||||
|
└── libs/ # Shared packages (@automaker/*)
|
||||||
|
├── types/ # Core TypeScript definitions (no dependencies)
|
||||||
|
├── utils/ # Logging, errors, image processing, context loading
|
||||||
|
├── prompts/ # AI prompt templates
|
||||||
|
├── platform/ # Path management, security, process spawning
|
||||||
|
├── model-resolver/ # Claude model alias resolution
|
||||||
|
├── dependency-resolver/ # Feature dependency ordering
|
||||||
|
└── git-utils/ # Git operations & worktree management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Dependency Chain
|
||||||
|
|
||||||
|
Packages can only depend on packages above them:
|
||||||
|
|
||||||
|
```
|
||||||
|
@automaker/types (no dependencies)
|
||||||
|
↓
|
||||||
|
@automaker/utils, @automaker/prompts, @automaker/platform, @automaker/model-resolver, @automaker/dependency-resolver
|
||||||
|
↓
|
||||||
|
@automaker/git-utils
|
||||||
|
↓
|
||||||
|
@automaker/server, @automaker/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Technologies
|
||||||
|
|
||||||
|
- **Frontend**: React 19, Vite 7, Electron 39, TanStack Router, Zustand 5, Tailwind CSS 4
|
||||||
|
- **Backend**: Express 5, WebSocket (ws), Claude Agent SDK, node-pty
|
||||||
|
- **Testing**: Playwright (E2E), Vitest (unit)
|
||||||
|
|
||||||
|
### Server Architecture
|
||||||
|
|
||||||
|
The server (`apps/server/src/`) follows a modular pattern:
|
||||||
|
|
||||||
|
- `routes/` - Express route handlers organized by feature (agent, features, auto-mode, worktree, etc.)
|
||||||
|
- `services/` - Business logic (AgentService, AutoModeService, FeatureLoader, TerminalService)
|
||||||
|
- `providers/` - AI provider abstraction (currently Claude via Claude Agent SDK)
|
||||||
|
- `lib/` - Utilities (events, auth, worktree metadata)
|
||||||
|
|
||||||
|
### Frontend Architecture
|
||||||
|
|
||||||
|
The UI (`apps/ui/src/`) uses:
|
||||||
|
|
||||||
|
- `routes/` - TanStack Router file-based routing
|
||||||
|
- `components/views/` - Main view components (board, settings, terminal, etc.)
|
||||||
|
- `store/` - Zustand stores with persistence (app-store.ts, setup-store.ts)
|
||||||
|
- `hooks/` - Custom React hooks
|
||||||
|
- `lib/` - Utilities and API client
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
### Per-Project Data (`.automaker/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
.automaker/
|
||||||
|
├── features/ # Feature JSON files and images
|
||||||
|
│ └── {featureId}/
|
||||||
|
│ ├── feature.json
|
||||||
|
│ ├── agent-output.md
|
||||||
|
│ └── images/
|
||||||
|
├── context/ # Context files for AI agents (CLAUDE.md, etc.)
|
||||||
|
├── settings.json # Project-specific settings
|
||||||
|
├── spec.md # Project specification
|
||||||
|
└── analysis.json # Project structure analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Data (`DATA_DIR`, default `./data`)
|
||||||
|
|
||||||
|
```
|
||||||
|
data/
|
||||||
|
├── settings.json # Global settings, profiles, shortcuts
|
||||||
|
├── credentials.json # API keys
|
||||||
|
├── sessions-metadata.json # Chat session metadata
|
||||||
|
└── agent-sessions/ # Conversation histories
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Conventions
|
||||||
|
|
||||||
|
Always import from shared packages, never from old paths:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
import type { Feature, ExecuteOptions } from '@automaker/types';
|
||||||
|
import { createLogger, classifyError } from '@automaker/utils';
|
||||||
|
import { getEnhancementPrompt } from '@automaker/prompts';
|
||||||
|
import { getFeatureDir, ensureAutomakerDir } from '@automaker/platform';
|
||||||
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
|
import { resolveDependencies } from '@automaker/dependency-resolver';
|
||||||
|
import { getGitRepositoryDiffs } from '@automaker/git-utils';
|
||||||
|
|
||||||
|
// ❌ Never import from old paths
|
||||||
|
import { Feature } from '../services/feature-loader'; // Wrong
|
||||||
|
import { createLogger } from '../lib/logger'; // Wrong
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### Event-Driven Architecture
|
||||||
|
|
||||||
|
All server operations emit events that stream to the frontend via WebSocket. Events are created using `createEventEmitter()` from `lib/events.ts`.
|
||||||
|
|
||||||
|
### Git Worktree Isolation
|
||||||
|
|
||||||
|
Each feature executes in an isolated git worktree, created via `@automaker/git-utils`. This protects the main branch during AI agent execution.
|
||||||
|
|
||||||
|
### Context Files
|
||||||
|
|
||||||
|
Project-specific rules are stored in `.automaker/context/` and automatically loaded into agent prompts via `loadContextFiles()` from `@automaker/utils`.
|
||||||
|
|
||||||
|
### Model Resolution
|
||||||
|
|
||||||
|
Use `resolveModelString()` from `@automaker/model-resolver` to convert model aliases:
|
||||||
|
|
||||||
|
- `haiku` → `claude-haiku-4-5`
|
||||||
|
- `sonnet` → `claude-sonnet-4-20250514`
|
||||||
|
- `opus` → `claude-opus-4-5-20251101`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `ANTHROPIC_API_KEY` - Anthropic API key (or use Claude Code CLI auth)
|
||||||
|
- `PORT` - Server port (default: 3008)
|
||||||
|
- `DATA_DIR` - Data storage directory (default: ./data)
|
||||||
|
- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory
|
||||||
|
- `AUTOMAKER_MOCK_AGENT=true` - Enable mock agent mode for CI testing
|
||||||
|
- `ENABLE_DEBUG_PANEL=true` - Enable debug panel in non-development builds
|
||||||
|
|
||||||
|
## Debug System (Development Only)
|
||||||
|
|
||||||
|
The debug system provides real-time monitoring of server performance. Toggle with `Cmd/Ctrl+Shift+D`.
|
||||||
|
|
||||||
|
### Debug Services (`apps/server/src/services/`)
|
||||||
|
|
||||||
|
- `PerformanceMonitorService` - Collects memory/CPU metrics, detects leaks via linear regression
|
||||||
|
- `ProcessRegistryService` - Tracks spawned agents, terminals, CLIs with lifecycle events
|
||||||
|
|
||||||
|
### Debug API (`apps/server/src/routes/debug/`)
|
||||||
|
|
||||||
|
- `GET /api/debug/metrics` - Current metrics snapshot
|
||||||
|
- `POST /api/debug/metrics/start` - Start collection with optional config
|
||||||
|
- `POST /api/debug/metrics/stop` - Stop collection
|
||||||
|
- `GET /api/debug/processes` - List tracked processes with filters
|
||||||
|
|
||||||
|
### Debug Types (`libs/types/src/debug.ts`)
|
||||||
|
|
||||||
|
All debug types are exported from `@automaker/types`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type {
|
||||||
|
DebugMetricsSnapshot,
|
||||||
|
TrackedProcess,
|
||||||
|
MemoryTrend,
|
||||||
|
ProcessSummary,
|
||||||
|
} from '@automaker/types';
|
||||||
|
import { formatBytes, formatDuration } from '@automaker/types';
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Components (`apps/ui/src/components/debug/`)
|
||||||
|
|
||||||
|
- `DebugPanel` - Main draggable container with tabs
|
||||||
|
- `MemoryMonitor` - Heap usage charts and leak indicators
|
||||||
|
- `CPUMonitor` - CPU gauge and event loop lag display
|
||||||
|
- `ProcessKanban` - Visual board of tracked processes
|
||||||
|
- `RenderTracker` - React component render statistics
|
||||||
|
|
||||||
|
See `docs/server/debug-api.md` for full API documentation.
|
||||||
685
CONTRIBUTING.md
Normal file
685
CONTRIBUTING.md
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
# Contributing to Automaker
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Automaker! We're excited to have you join our community of developers building the future of autonomous AI development.
|
||||||
|
|
||||||
|
Automaker is an autonomous AI development studio that provides a Kanban-based workflow where AI agents implement features in isolated git worktrees. Whether you're fixing bugs, adding features, improving documentation, or suggesting ideas, your contributions help make this project better for everyone.
|
||||||
|
|
||||||
|
This guide will help you get started with contributing to Automaker. Please take a moment to read through these guidelines to ensure a smooth contribution process.
|
||||||
|
|
||||||
|
## Contribution License Agreement
|
||||||
|
|
||||||
|
**Important:** By submitting, pushing, or contributing any code, documentation, pull requests, issues, or other materials to the Automaker project, you agree to assign all right, title, and interest in and to your contributions, including all copyrights, patents, and other intellectual property rights, to the Core Contributors of Automaker. This assignment is irrevocable and includes the right to use, modify, distribute, and monetize your contributions in any manner.
|
||||||
|
|
||||||
|
**You understand and agree that you will have no right to receive any royalties, compensation, or other financial benefits from any revenue, income, or commercial use generated from your contributed code or any derivative works thereof.** All contributions are made without expectation of payment or financial return.
|
||||||
|
|
||||||
|
For complete details on contribution terms and rights assignment, please review [Section 5 (CONTRIBUTIONS AND RIGHTS ASSIGNMENT) of the LICENSE](LICENSE#5-contributions-and-rights-assignment).
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Contributing to Automaker](#contributing-to-automaker)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Fork and Clone](#fork-and-clone)
|
||||||
|
- [Development Setup](#development-setup)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Pull Request Process](#pull-request-process)
|
||||||
|
- [Branch Naming Convention](#branch-naming-convention)
|
||||||
|
- [Commit Message Format](#commit-message-format)
|
||||||
|
- [Submitting a Pull Request](#submitting-a-pull-request)
|
||||||
|
- [1. Prepare Your Changes](#1-prepare-your-changes)
|
||||||
|
- [2. Run Pre-submission Checks](#2-run-pre-submission-checks)
|
||||||
|
- [3. Push Your Changes](#3-push-your-changes)
|
||||||
|
- [4. Open a Pull Request](#4-open-a-pull-request)
|
||||||
|
- [PR Requirements Checklist](#pr-requirements-checklist)
|
||||||
|
- [Review Process](#review-process)
|
||||||
|
- [What to Expect](#what-to-expect)
|
||||||
|
- [Review Focus Areas](#review-focus-areas)
|
||||||
|
- [Responding to Feedback](#responding-to-feedback)
|
||||||
|
- [Approval Criteria](#approval-criteria)
|
||||||
|
- [Getting Help](#getting-help)
|
||||||
|
- [Code Style Guidelines](#code-style-guidelines)
|
||||||
|
- [Testing Requirements](#testing-requirements)
|
||||||
|
- [Running Tests](#running-tests)
|
||||||
|
- [Test Frameworks](#test-frameworks)
|
||||||
|
- [End-to-End Tests (Playwright)](#end-to-end-tests-playwright)
|
||||||
|
- [Unit Tests (Vitest)](#unit-tests-vitest)
|
||||||
|
- [Writing Tests](#writing-tests)
|
||||||
|
- [When to Write Tests](#when-to-write-tests)
|
||||||
|
- [CI/CD Pipeline](#cicd-pipeline)
|
||||||
|
- [CI Checks](#ci-checks)
|
||||||
|
- [CI Testing Environment](#ci-testing-environment)
|
||||||
|
- [Viewing CI Results](#viewing-ci-results)
|
||||||
|
- [Common CI Failures](#common-ci-failures)
|
||||||
|
- [Coverage Requirements](#coverage-requirements)
|
||||||
|
- [Issue Reporting](#issue-reporting)
|
||||||
|
- [Bug Reports](#bug-reports)
|
||||||
|
- [Before Reporting](#before-reporting)
|
||||||
|
- [Bug Report Template](#bug-report-template)
|
||||||
|
- [Feature Requests](#feature-requests)
|
||||||
|
- [Before Requesting](#before-requesting)
|
||||||
|
- [Feature Request Template](#feature-request-template)
|
||||||
|
- [Security Issues](#security-issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Before contributing to Automaker, ensure you have the following installed on your system:
|
||||||
|
|
||||||
|
- **Node.js 18+** (tested with Node.js 22)
|
||||||
|
- Download from [nodejs.org](https://nodejs.org/)
|
||||||
|
- Verify installation: `node --version`
|
||||||
|
- **npm** (comes with Node.js)
|
||||||
|
- Verify installation: `npm --version`
|
||||||
|
- **Git** for version control
|
||||||
|
- Verify installation: `git --version`
|
||||||
|
- **Claude Code CLI** or **Anthropic API Key** (for AI agent functionality)
|
||||||
|
- Required to run the AI development features
|
||||||
|
|
||||||
|
**Optional but recommended:**
|
||||||
|
|
||||||
|
- A code editor with TypeScript support (VS Code recommended)
|
||||||
|
- GitHub CLI (`gh`) for easier PR management
|
||||||
|
|
||||||
|
### Fork and Clone
|
||||||
|
|
||||||
|
1. **Fork the repository** on GitHub
|
||||||
|
- Navigate to [https://github.com/AutoMaker-Org/automaker](https://github.com/AutoMaker-Org/automaker)
|
||||||
|
- Click the "Fork" button in the top-right corner
|
||||||
|
- This creates your own copy of the repository
|
||||||
|
|
||||||
|
2. **Clone your fork locally**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/automaker.git
|
||||||
|
cd automaker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add the upstream remote** to keep your fork in sync
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/AutoMaker-Org/automaker.git
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify remotes**
|
||||||
|
```bash
|
||||||
|
git remote -v
|
||||||
|
# Should show:
|
||||||
|
# origin https://github.com/YOUR_USERNAME/automaker.git (fetch)
|
||||||
|
# origin https://github.com/YOUR_USERNAME/automaker.git (push)
|
||||||
|
# upstream https://github.com/AutoMaker-Org/automaker.git (fetch)
|
||||||
|
# upstream https://github.com/AutoMaker-Org/automaker.git (push)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build shared packages** (required before running the app)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:packages
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the development server**
|
||||||
|
```bash
|
||||||
|
npm run dev # Interactive launcher - choose mode
|
||||||
|
npm run dev:web # Browser mode (web interface)
|
||||||
|
npm run dev:electron # Desktop app mode
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common development commands:**
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------------------ | -------------------------------- |
|
||||||
|
| `npm run dev` | Interactive development launcher |
|
||||||
|
| `npm run dev:web` | Start in browser mode |
|
||||||
|
| `npm run dev:electron` | Start desktop app |
|
||||||
|
| `npm run build` | Build all packages and apps |
|
||||||
|
| `npm run build:packages` | Build shared packages only |
|
||||||
|
| `npm run lint` | Run ESLint checks |
|
||||||
|
| `npm run format` | Format code with Prettier |
|
||||||
|
| `npm run format:check` | Check formatting without changes |
|
||||||
|
| `npm run test` | Run E2E tests (Playwright) |
|
||||||
|
| `npm run test:server` | Run server unit tests |
|
||||||
|
| `npm run test:packages` | Run package tests |
|
||||||
|
| `npm run test:all` | Run all tests |
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
Automaker is organized as an npm workspace monorepo:
|
||||||
|
|
||||||
|
```
|
||||||
|
automaker/
|
||||||
|
├── apps/
|
||||||
|
│ ├── ui/ # React + Vite + Electron frontend
|
||||||
|
│ └── server/ # Express + WebSocket backend
|
||||||
|
├── libs/
|
||||||
|
│ ├── @automaker/types/ # Shared TypeScript types
|
||||||
|
│ ├── @automaker/utils/ # Utility functions
|
||||||
|
│ ├── @automaker/prompts/ # AI prompt templates
|
||||||
|
│ ├── @automaker/platform/ # Platform abstractions
|
||||||
|
│ ├── @automaker/model-resolver/ # AI model resolution
|
||||||
|
│ ├── @automaker/dependency-resolver/ # Dependency management
|
||||||
|
│ └── @automaker/git-utils/ # Git operations
|
||||||
|
├── docs/ # Documentation
|
||||||
|
└── package.json # Root package configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key conventions:**
|
||||||
|
|
||||||
|
- Always import from `@automaker/*` shared packages, never use relative paths to `libs/`
|
||||||
|
- Frontend code lives in `apps/ui/`
|
||||||
|
- Backend code lives in `apps/server/`
|
||||||
|
- Shared logic should be in the appropriate `libs/` package
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
This section covers everything you need to know about contributing changes through pull requests, from creating your branch to getting your code merged.
|
||||||
|
|
||||||
|
### Branch Naming Convention
|
||||||
|
|
||||||
|
We use a consistent branch naming pattern to keep our repository organized:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>/<description>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Branch types:**
|
||||||
|
|
||||||
|
| Type | Purpose | Example |
|
||||||
|
| ---------- | ------------------------ | --------------------------------- |
|
||||||
|
| `feature` | New functionality | `feature/add-user-authentication` |
|
||||||
|
| `fix` | Bug fixes | `fix/resolve-memory-leak` |
|
||||||
|
| `docs` | Documentation changes | `docs/update-contributing-guide` |
|
||||||
|
| `refactor` | Code restructuring | `refactor/simplify-api-handlers` |
|
||||||
|
| `test` | Adding or updating tests | `test/add-utils-unit-tests` |
|
||||||
|
| `chore` | Maintenance tasks | `chore/update-dependencies` |
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Use lowercase letters and hyphens (no underscores or spaces)
|
||||||
|
- Keep descriptions short but descriptive
|
||||||
|
- Include issue number when applicable: `feature/123-add-login`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create and checkout a new feature branch
|
||||||
|
git checkout -b feature/add-dark-mode
|
||||||
|
|
||||||
|
# Create a fix branch with issue reference
|
||||||
|
git checkout -b fix/456-resolve-login-error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
We follow the **Conventional Commits** style for clear, readable commit history:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit types:**
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
| ---------- | --------------------------- |
|
||||||
|
| `feat` | New feature |
|
||||||
|
| `fix` | Bug fix |
|
||||||
|
| `docs` | Documentation only |
|
||||||
|
| `style` | Formatting (no code change) |
|
||||||
|
| `refactor` | Code restructuring |
|
||||||
|
| `test` | Adding or updating tests |
|
||||||
|
| `chore` | Maintenance tasks |
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Use **imperative mood** ("Add feature" not "Added feature")
|
||||||
|
- Keep first line under **72 characters**
|
||||||
|
- Capitalize the first letter after the type prefix
|
||||||
|
- No period at the end of the subject line
|
||||||
|
- Add a blank line before the body for detailed explanations
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple commit
|
||||||
|
git commit -m "feat: Add user authentication flow"
|
||||||
|
|
||||||
|
# Commit with body for more context
|
||||||
|
git commit -m "fix: Resolve memory leak in WebSocket handler
|
||||||
|
|
||||||
|
The connection cleanup was not being called when clients
|
||||||
|
disconnected unexpectedly. Added proper cleanup in the
|
||||||
|
error handler to prevent memory accumulation."
|
||||||
|
|
||||||
|
# Documentation update
|
||||||
|
git commit -m "docs: Update API documentation"
|
||||||
|
|
||||||
|
# Refactoring
|
||||||
|
git commit -m "refactor: Simplify state management logic"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Submitting a Pull Request
|
||||||
|
|
||||||
|
Follow these steps to submit your contribution:
|
||||||
|
|
||||||
|
#### 1. Prepare Your Changes
|
||||||
|
|
||||||
|
Ensure you've synced with the latest upstream changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fetch latest changes from upstream
|
||||||
|
git fetch upstream
|
||||||
|
|
||||||
|
# Rebase your branch on main (if needed)
|
||||||
|
git rebase upstream/main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Run Pre-submission Checks
|
||||||
|
|
||||||
|
Before opening your PR, verify everything passes locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm run test:all
|
||||||
|
|
||||||
|
# Check formatting
|
||||||
|
npm run format:check
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Build to verify no compile errors
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Push Your Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push your branch to your fork
|
||||||
|
git push origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Open a Pull Request
|
||||||
|
|
||||||
|
1. Go to your fork on GitHub
|
||||||
|
2. Click "Compare & pull request" for your branch
|
||||||
|
3. Ensure the base repository is `AutoMaker-Org/automaker` and base branch is `main`
|
||||||
|
4. Fill out the PR template completely
|
||||||
|
|
||||||
|
#### PR Requirements Checklist
|
||||||
|
|
||||||
|
Your PR should include:
|
||||||
|
|
||||||
|
- [ ] **Clear title** describing the change (use conventional commit format)
|
||||||
|
- [ ] **Description** explaining what changed and why
|
||||||
|
- [ ] **Link to related issue** (if applicable): `Closes #123` or `Fixes #456`
|
||||||
|
- [ ] **All CI checks passing** (format, lint, build, tests)
|
||||||
|
- [ ] **No merge conflicts** with main branch
|
||||||
|
- [ ] **Tests included** for new functionality
|
||||||
|
- [ ] **Documentation updated** if adding/changing public APIs
|
||||||
|
|
||||||
|
**Example PR Description:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This PR adds dark mode support to the Automaker UI.
|
||||||
|
|
||||||
|
- Implements theme toggle in settings panel
|
||||||
|
- Adds CSS custom properties for theme colors
|
||||||
|
- Persists theme preference to localStorage
|
||||||
|
|
||||||
|
## Related Issue
|
||||||
|
|
||||||
|
Closes #123
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [x] Tested toggle functionality in Chrome and Firefox
|
||||||
|
- [x] Verified theme persists across page reloads
|
||||||
|
- [x] Checked accessibility contrast ratios
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
[Include before/after screenshots for UI changes]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Process
|
||||||
|
|
||||||
|
All contributions go through code review to maintain quality:
|
||||||
|
|
||||||
|
#### What to Expect
|
||||||
|
|
||||||
|
1. **CI Checks Run First** - Automated checks (format, lint, build, tests) must pass before review
|
||||||
|
2. **Maintainer Review** - The project maintainers will review your PR and decide whether to merge it
|
||||||
|
3. **Feedback & Discussion** - The reviewer may ask questions or request changes
|
||||||
|
4. **Iteration** - Make requested changes and push updates to the same branch
|
||||||
|
5. **Approval & Merge** - Once approved and checks pass, your PR will be merged
|
||||||
|
|
||||||
|
#### Review Focus Areas
|
||||||
|
|
||||||
|
The reviewer checks for:
|
||||||
|
|
||||||
|
- **Correctness** - Does the code work as intended?
|
||||||
|
- **Clean Code** - Does it follow our [code style guidelines](#code-style-guidelines)?
|
||||||
|
- **Test Coverage** - Are new features properly tested?
|
||||||
|
- **Documentation** - Are public APIs documented?
|
||||||
|
- **Breaking Changes** - Are any breaking changes discussed first?
|
||||||
|
|
||||||
|
#### Responding to Feedback
|
||||||
|
|
||||||
|
- Respond to **all** review comments, even if just to acknowledge
|
||||||
|
- Ask questions if feedback is unclear
|
||||||
|
- Push additional commits to address feedback (don't force-push during review)
|
||||||
|
- Mark conversations as resolved once addressed
|
||||||
|
|
||||||
|
#### Approval Criteria
|
||||||
|
|
||||||
|
Your PR is ready to merge when:
|
||||||
|
|
||||||
|
- ✅ All CI checks pass
|
||||||
|
- ✅ The maintainer has approved the changes
|
||||||
|
- ✅ All review comments are addressed
|
||||||
|
- ✅ No unresolved merge conflicts
|
||||||
|
|
||||||
|
#### Getting Help
|
||||||
|
|
||||||
|
If your PR seems stuck:
|
||||||
|
|
||||||
|
- Comment asking for status update (mention @webdevcody if needed)
|
||||||
|
- Reach out on [Discord](https://discord.gg/jjem7aEDKU)
|
||||||
|
- Make sure all checks are passing and you've responded to all feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
Automaker uses automated tooling to enforce code style. Run `npm run format` to format code and `npm run lint` to check for issues. Pre-commit hooks automatically format staged files before committing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
Testing helps prevent regressions. Automaker uses **Playwright** for end-to-end testing and **Vitest** for unit tests.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
Use these commands to run tests locally:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------------------------ | ------------------------------------- |
|
||||||
|
| `npm run test` | Run E2E tests (Playwright) |
|
||||||
|
| `npm run test:server` | Run server unit tests (Vitest) |
|
||||||
|
| `npm run test:packages` | Run shared package tests |
|
||||||
|
| `npm run test:all` | Run all tests |
|
||||||
|
| `npm run test:server:coverage` | Run server tests with coverage report |
|
||||||
|
|
||||||
|
**Before submitting a PR**, always run the full test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Frameworks
|
||||||
|
|
||||||
|
#### End-to-End Tests (Playwright)
|
||||||
|
|
||||||
|
E2E tests verify the entire application works correctly from a user's perspective.
|
||||||
|
|
||||||
|
- **Framework:** [Playwright](https://playwright.dev/)
|
||||||
|
- **Location:** `e2e/` directory
|
||||||
|
- **Test ports:** UI on port 3007, Server on port 3008
|
||||||
|
|
||||||
|
**Running E2E tests:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all E2E tests
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Run with headed browser (useful for debugging)
|
||||||
|
npx playwright test --headed
|
||||||
|
|
||||||
|
# Run a specific test file
|
||||||
|
npm test --workspace=@automaker/ui -- tests/example.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**E2E Test Guidelines:**
|
||||||
|
|
||||||
|
- Write tests from a user's perspective
|
||||||
|
- Use descriptive test names that explain the scenario
|
||||||
|
- Clean up test data after each test
|
||||||
|
- Use appropriate timeouts for async operations
|
||||||
|
- Prefer `locator` over direct selectors for resilience
|
||||||
|
|
||||||
|
#### Unit Tests (Vitest)
|
||||||
|
|
||||||
|
Unit tests verify individual functions and modules work correctly in isolation.
|
||||||
|
|
||||||
|
- **Framework:** [Vitest](https://vitest.dev/)
|
||||||
|
- **Location:** In the `tests/` directory within each package (e.g., `apps/server/tests/`)
|
||||||
|
|
||||||
|
**Running unit tests:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all server unit tests
|
||||||
|
npm run test:server
|
||||||
|
|
||||||
|
# Run with coverage report
|
||||||
|
npm run test:server:coverage
|
||||||
|
|
||||||
|
# Run package tests
|
||||||
|
npm run test:packages
|
||||||
|
|
||||||
|
# Run in watch mode during development
|
||||||
|
npx vitest --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unit Test Guidelines:**
|
||||||
|
|
||||||
|
- Keep tests small and focused on one behavior
|
||||||
|
- Use descriptive test names: `it('should return null when user is not found')`
|
||||||
|
- Follow the AAA pattern: Arrange, Act, Assert
|
||||||
|
- Mock external dependencies to isolate the unit under test
|
||||||
|
- Aim for meaningful coverage, not just line coverage
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
#### When to Write Tests
|
||||||
|
|
||||||
|
- **New features:** All new features should include tests
|
||||||
|
- **Bug fixes:** Add a test that reproduces the bug before fixing
|
||||||
|
- **Refactoring:** Ensure existing tests pass after refactoring
|
||||||
|
- **Public APIs:** All public APIs must have test coverage
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
Automaker uses **GitHub Actions** for continuous integration. Every pull request triggers automated checks.
|
||||||
|
|
||||||
|
#### CI Checks
|
||||||
|
|
||||||
|
The following checks must pass before your PR can be merged:
|
||||||
|
|
||||||
|
| Check | Description |
|
||||||
|
| ----------------- | --------------------------------------------- |
|
||||||
|
| **Format** | Verifies code is formatted with Prettier |
|
||||||
|
| **Build** | Ensures the project compiles without errors |
|
||||||
|
| **Package Tests** | Runs tests for shared `@automaker/*` packages |
|
||||||
|
| **Server Tests** | Runs server unit tests with coverage |
|
||||||
|
|
||||||
|
#### CI Testing Environment
|
||||||
|
|
||||||
|
For CI environments, Automaker supports a mock agent mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable mock agent mode for CI testing
|
||||||
|
AUTOMAKER_MOCK_AGENT=true npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows tests to run without requiring a real Claude API connection.
|
||||||
|
|
||||||
|
#### Viewing CI Results
|
||||||
|
|
||||||
|
1. Go to your PR on GitHub
|
||||||
|
2. Scroll to the "Checks" section at the bottom
|
||||||
|
3. Click on any failed check to see detailed logs
|
||||||
|
4. Fix issues locally and push updates
|
||||||
|
|
||||||
|
#### Common CI Failures
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
| ------------------- | --------------------------------------------- |
|
||||||
|
| Format check failed | Run `npm run format` locally |
|
||||||
|
| Build failed | Run `npm run build` and fix TypeScript errors |
|
||||||
|
| Tests failed | Run `npm run test:all` locally to reproduce |
|
||||||
|
| Coverage decreased | Add tests for new code paths |
|
||||||
|
|
||||||
|
### Coverage Requirements
|
||||||
|
|
||||||
|
While we don't enforce strict coverage percentages, we expect:
|
||||||
|
|
||||||
|
- **New features:** Should include comprehensive tests
|
||||||
|
- **Bug fixes:** Should include a regression test
|
||||||
|
- **Critical paths:** Must have test coverage (authentication, data persistence, etc.)
|
||||||
|
|
||||||
|
To view coverage reports locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:server:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
This generates an HTML report you can open in your browser to see which lines are covered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Reporting
|
||||||
|
|
||||||
|
Found a bug or have an idea for a new feature? We'd love to hear from you! This section explains how to report issues effectively.
|
||||||
|
|
||||||
|
### Bug Reports
|
||||||
|
|
||||||
|
When reporting a bug, please provide as much information as possible to help us understand and reproduce the issue.
|
||||||
|
|
||||||
|
#### Before Reporting
|
||||||
|
|
||||||
|
1. **Search existing issues** - Check if the bug has already been reported
|
||||||
|
2. **Try the latest version** - Make sure you're running the latest version of Automaker
|
||||||
|
3. **Reproduce the issue** - Verify you can consistently reproduce the bug
|
||||||
|
|
||||||
|
#### Bug Report Template
|
||||||
|
|
||||||
|
When creating a bug report, include:
|
||||||
|
|
||||||
|
- **Title:** A clear, descriptive title summarizing the issue
|
||||||
|
- **Environment:**
|
||||||
|
- Operating System and version
|
||||||
|
- Node.js version (`node --version`)
|
||||||
|
- Automaker version or commit hash
|
||||||
|
- **Steps to Reproduce:** Numbered list of steps to reproduce the bug
|
||||||
|
- **Expected Behavior:** What you expected to happen
|
||||||
|
- **Actual Behavior:** What actually happened
|
||||||
|
- **Logs/Screenshots:** Any relevant error messages, console output, or screenshots
|
||||||
|
|
||||||
|
**Example Bug Report:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Bug: WebSocket connection drops after 5 minutes of inactivity
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
- OS: Windows 11
|
||||||
|
- Node.js: 22.11.0
|
||||||
|
- Automaker: commit abc1234
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
1. Start the application with `npm run dev:web`
|
||||||
|
2. Open the Kanban board
|
||||||
|
3. Leave the browser tab open for 5+ minutes without interaction
|
||||||
|
4. Try to move a card
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
|
||||||
|
The card should move to the new column.
|
||||||
|
|
||||||
|
### Actual Behavior
|
||||||
|
|
||||||
|
The UI shows "Connection lost" and the card doesn't move.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
[WebSocket] Connection closed: 1006
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Requests
|
||||||
|
|
||||||
|
We welcome ideas for improving Automaker! Here's how to submit a feature request:
|
||||||
|
|
||||||
|
#### Before Requesting
|
||||||
|
|
||||||
|
1. **Check existing issues** - Your idea may already be proposed or in development
|
||||||
|
2. **Consider scope** - Think about whether the feature fits Automaker's mission as an autonomous AI development studio
|
||||||
|
|
||||||
|
#### Feature Request Template
|
||||||
|
|
||||||
|
A good feature request includes:
|
||||||
|
|
||||||
|
- **Title:** A brief, descriptive title
|
||||||
|
- **Problem Statement:** What problem does this feature solve?
|
||||||
|
- **Proposed Solution:** How do you envision this working?
|
||||||
|
- **Alternatives Considered:** What other approaches did you consider?
|
||||||
|
- **Additional Context:** Mockups, examples, or references that help explain your idea
|
||||||
|
|
||||||
|
**Example Feature Request:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Feature: Dark Mode Support
|
||||||
|
|
||||||
|
### Problem Statement
|
||||||
|
|
||||||
|
Working late at night, the bright UI causes eye strain and doesn't match
|
||||||
|
my system's dark theme preference.
|
||||||
|
|
||||||
|
### Proposed Solution
|
||||||
|
|
||||||
|
Add a theme toggle in the settings panel that allows switching between
|
||||||
|
light and dark modes. Ideally, it should also detect system preference.
|
||||||
|
|
||||||
|
### Alternatives Considered
|
||||||
|
|
||||||
|
- Browser extension to force dark mode (doesn't work well with custom styling)
|
||||||
|
- Custom CSS override (breaks with updates)
|
||||||
|
|
||||||
|
### Additional Context
|
||||||
|
|
||||||
|
Similar to how VS Code handles themes - a dropdown in settings with
|
||||||
|
immediate preview.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Issues
|
||||||
|
|
||||||
|
**Important:** If you discover a security vulnerability, please do NOT open a public issue. Instead:
|
||||||
|
|
||||||
|
1. Join our [Discord server](https://discord.gg/jjem7aEDKU) and send a direct message to the user `@webdevcody`
|
||||||
|
2. Include detailed steps to reproduce
|
||||||
|
3. Allow time for us to address the issue before public disclosure
|
||||||
|
|
||||||
|
We take security seriously and appreciate responsible disclosure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For license and contribution terms, see the [LICENSE](LICENSE) file in the repository root and the [README.md](README.md#license) for more details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for contributing to Automaker!
|
||||||
@@ -19,30 +19,57 @@ While we have made efforts to review this codebase for security vulnerabilities
|
|||||||
## Recommendations
|
## Recommendations
|
||||||
|
|
||||||
### 1. Review the Code First
|
### 1. Review the Code First
|
||||||
|
|
||||||
Before running Automaker, we strongly recommend reviewing the source code yourself to understand what operations it performs and ensure you are comfortable with its behavior.
|
Before running Automaker, we strongly recommend reviewing the source code yourself to understand what operations it performs and ensure you are comfortable with its behavior.
|
||||||
|
|
||||||
### 2. Use Sandboxing (Highly Recommended)
|
### 2. Use Sandboxing (Highly Recommended)
|
||||||
|
|
||||||
**We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Instead, consider:
|
**We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Instead, consider:
|
||||||
|
|
||||||
- **Docker**: Run Automaker in a Docker container to isolate it from your host system
|
- **Docker**: Run Automaker in a Docker container to isolate it from your host system
|
||||||
- **Virtual Machine**: Use a VM (such as VirtualBox, VMware, or Parallels) to create an isolated environment
|
- **Virtual Machine**: Use a VM (such as VirtualBox, VMware, or Parallels) to create an isolated environment
|
||||||
- **Cloud Development Environment**: Use a cloud-based development environment that provides isolation
|
- **Cloud Development Environment**: Use a cloud-based development environment that provides isolation
|
||||||
|
|
||||||
|
#### Running in Isolated Docker Container
|
||||||
|
|
||||||
|
For maximum security, run Automaker in an isolated Docker container that **cannot access your laptop's files**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Set your API key (bash/Linux/Mac - creates UTF-8 file)
|
||||||
|
echo "ANTHROPIC_API_KEY=your-api-key-here" > .env
|
||||||
|
|
||||||
|
# On Windows PowerShell, use instead:
|
||||||
|
Set-Content -Path .env -Value "ANTHROPIC_API_KEY=your-api-key-here" -Encoding UTF8
|
||||||
|
|
||||||
|
# 2. Build and run isolated container
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 3. Access the UI at http://localhost:3007
|
||||||
|
# API at http://localhost:3008/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
The container uses only Docker-managed volumes and has no access to your host filesystem. See [docker-isolation.md](docs/docker-isolation.md) for full documentation.
|
||||||
|
|
||||||
### 3. Limit Access
|
### 3. Limit Access
|
||||||
|
|
||||||
If you must run locally:
|
If you must run locally:
|
||||||
|
|
||||||
- Create a dedicated user account with limited permissions
|
- Create a dedicated user account with limited permissions
|
||||||
- Only grant access to specific project directories
|
- Only grant access to specific project directories
|
||||||
- Avoid running with administrator/root privileges
|
- Avoid running with administrator/root privileges
|
||||||
- Keep sensitive files and credentials outside of project directories
|
- Keep sensitive files and credentials outside of project directories
|
||||||
|
|
||||||
### 4. Monitor Activity
|
### 4. Monitor Activity
|
||||||
|
|
||||||
- Review the agent's actions in the output logs
|
- Review the agent's actions in the output logs
|
||||||
- Pay attention to file modifications and command executions
|
- Pay attention to file modifications and command executions
|
||||||
- Stop the agent immediately if you notice unexpected behavior
|
- Stop the agent immediately if you notice unexpected behavior
|
||||||
|
|
||||||
## No Warranty
|
## No Warranty & Limitation of Liability
|
||||||
|
|
||||||
This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
|
THE SOFTWARE UTILIZES ARTIFICIAL INTELLIGENCE TO GENERATE CODE, EXECUTE COMMANDS, AND INTERACT WITH YOUR FILE SYSTEM. YOU ACKNOWLEDGE THAT AI SYSTEMS CAN BE UNPREDICTABLE, MAY GENERATE INCORRECT, INSECURE, OR DESTRUCTIVE CODE, AND MAY TAKE ACTIONS THAT COULD DAMAGE YOUR SYSTEM, FILES, OR HARDWARE.
|
||||||
|
|
||||||
|
This software is provided "as is", without warranty of any kind, express or implied. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, including but not limited to hardware damage, data loss, financial loss, or business interruption, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
|
||||||
|
|
||||||
## Acknowledgment
|
## Acknowledgment
|
||||||
|
|
||||||
|
|||||||
154
Dockerfile
Normal file
154
Dockerfile
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Automaker Multi-Stage Dockerfile
|
||||||
|
# Single Dockerfile for both server and UI builds
|
||||||
|
# Usage:
|
||||||
|
# docker build --target server -t automaker-server .
|
||||||
|
# docker build --target ui -t automaker-ui .
|
||||||
|
# Or use docker-compose which selects targets automatically
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BASE STAGE - Common setup for all builds (DRY: defined once, used by all)
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
|
# Install build dependencies for native modules (node-pty)
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy root package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Copy all libs package.json files (centralized - add new libs here)
|
||||||
|
COPY libs/types/package*.json ./libs/types/
|
||||||
|
COPY libs/utils/package*.json ./libs/utils/
|
||||||
|
COPY libs/prompts/package*.json ./libs/prompts/
|
||||||
|
COPY libs/platform/package*.json ./libs/platform/
|
||||||
|
COPY libs/model-resolver/package*.json ./libs/model-resolver/
|
||||||
|
COPY libs/dependency-resolver/package*.json ./libs/dependency-resolver/
|
||||||
|
COPY libs/git-utils/package*.json ./libs/git-utils/
|
||||||
|
|
||||||
|
# Copy scripts (needed by npm workspace)
|
||||||
|
COPY scripts ./scripts
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SERVER BUILD STAGE
|
||||||
|
# =============================================================================
|
||||||
|
FROM base AS server-builder
|
||||||
|
|
||||||
|
# Copy server-specific package.json
|
||||||
|
COPY apps/server/package*.json ./apps/server/
|
||||||
|
|
||||||
|
# Install dependencies (--ignore-scripts to skip husky/prepare, then rebuild native modules)
|
||||||
|
RUN npm ci --ignore-scripts && npm rebuild node-pty
|
||||||
|
|
||||||
|
# Copy all source files
|
||||||
|
COPY libs ./libs
|
||||||
|
COPY apps/server ./apps/server
|
||||||
|
|
||||||
|
# Build packages in dependency order, then build server
|
||||||
|
RUN npm run build:packages && npm run build --workspace=apps/server
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SERVER PRODUCTION STAGE
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:22-alpine AS server
|
||||||
|
|
||||||
|
# Install git, curl, bash (for terminal), and GitHub CLI (pinned version, multi-arch)
|
||||||
|
RUN apk add --no-cache git curl bash && \
|
||||||
|
GH_VERSION="2.63.2" && \
|
||||||
|
ARCH=$(uname -m) && \
|
||||||
|
case "$ARCH" in \
|
||||||
|
x86_64) GH_ARCH="amd64" ;; \
|
||||||
|
aarch64|arm64) GH_ARCH="arm64" ;; \
|
||||||
|
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
|
||||||
|
esac && \
|
||||||
|
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" -o gh.tar.gz && \
|
||||||
|
tar -xzf gh.tar.gz && \
|
||||||
|
mv gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh /usr/local/bin/gh && \
|
||||||
|
rm -rf gh.tar.gz gh_${GH_VERSION}_linux_${GH_ARCH}
|
||||||
|
|
||||||
|
# Install Claude CLI globally
|
||||||
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S automaker && \
|
||||||
|
adduser -S automaker -u 1001
|
||||||
|
|
||||||
|
# Copy root package.json (needed for workspace resolution)
|
||||||
|
COPY --from=server-builder /app/package*.json ./
|
||||||
|
|
||||||
|
# Copy built libs (workspace packages are symlinked in node_modules)
|
||||||
|
COPY --from=server-builder /app/libs ./libs
|
||||||
|
|
||||||
|
# Copy built server
|
||||||
|
COPY --from=server-builder /app/apps/server/dist ./apps/server/dist
|
||||||
|
COPY --from=server-builder /app/apps/server/package*.json ./apps/server/
|
||||||
|
|
||||||
|
# Copy node_modules (includes symlinks to libs)
|
||||||
|
COPY --from=server-builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Create data and projects directories
|
||||||
|
RUN mkdir -p /data /projects && chown automaker:automaker /data /projects
|
||||||
|
|
||||||
|
# Configure git for mounted volumes and authentication
|
||||||
|
# Use --system so it's not overwritten by mounted user .gitconfig
|
||||||
|
RUN git config --system --add safe.directory '*' && \
|
||||||
|
# Use gh as credential helper (works with GH_TOKEN env var)
|
||||||
|
git config --system credential.helper '!gh auth git-credential'
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER automaker
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV PORT=3008
|
||||||
|
ENV DATA_DIR=/data
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3008
|
||||||
|
|
||||||
|
# Health check (using curl since it's already installed, more reliable than busybox wget)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3008/api/health || exit 1
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
CMD ["node", "apps/server/dist/index.js"]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI BUILD STAGE
|
||||||
|
# =============================================================================
|
||||||
|
FROM base AS ui-builder
|
||||||
|
|
||||||
|
# Copy UI-specific package.json
|
||||||
|
COPY apps/ui/package*.json ./apps/ui/
|
||||||
|
|
||||||
|
# Install dependencies (--ignore-scripts to skip husky and build:packages in prepare script)
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
|
# Copy all source files
|
||||||
|
COPY libs ./libs
|
||||||
|
COPY apps/ui ./apps/ui
|
||||||
|
|
||||||
|
# Build packages in dependency order, then build UI
|
||||||
|
# VITE_SERVER_URL tells the UI where to find the API server
|
||||||
|
# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com
|
||||||
|
ARG VITE_SERVER_URL=http://localhost:3008
|
||||||
|
ENV VITE_SKIP_ELECTRON=true
|
||||||
|
ENV VITE_SERVER_URL=${VITE_SERVER_URL}
|
||||||
|
RUN npm run build:packages && npm run build --workspace=apps/ui
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI PRODUCTION STAGE
|
||||||
|
# =============================================================================
|
||||||
|
FROM nginx:alpine AS ui
|
||||||
|
|
||||||
|
# Copy built files
|
||||||
|
COPY --from=ui-builder /app/apps/ui/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx config for SPA routing
|
||||||
|
COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
241
LICENSE
241
LICENSE
@@ -1,208 +1,141 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
AUTOMAKER LICENSE AGREEMENT
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
This License Agreement ("Agreement") is entered into between you ("Licensee") and the copyright holders of Automaker ("Licensor"). By using, copying, modifying, downloading, cloning, or distributing the Software (as defined below), you agree to be bound by the terms of this Agreement.
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
1. DEFINITIONS
|
||||||
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
|
"Software" means the Automaker software, including all source code, object code, documentation, and related materials.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
|
"Generated Files" means files created by the Software during normal operation to store internal state, configuration, or working data, including but not limited to app_spec.txt, feature.json, and similar files generated by the Software. Generated Files are not considered part of the Software for the purposes of this license and are not subject to the restrictions herein.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
|
"Derivative Work" means any work that is based on, derived from, or incorporates the Software or any substantial portion of it, including but not limited to modifications, forks, adaptations, translations, or any altered version of the Software.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
|
"Monetization" means any activity that generates revenue, income, or commercial benefit from the Software itself or any Derivative Work, including but not limited to:
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
|
- Reselling, redistributing, or sublicensing the Software, any Derivative Work, or any substantial portion thereof
|
||||||
|
- Including the Software, any Derivative Work, or substantial portions thereof in a product or service that you sell or distribute
|
||||||
|
- Offering the Software, any Derivative Work, or substantial portions thereof as a standalone product or service for sale
|
||||||
|
- Hosting the Software or any Derivative Work as a service (whether free or paid) for use by others, including cloud hosting, Software-as-a-Service (SaaS), or any other form of hosted access for third parties
|
||||||
|
- Extracting, reselling, redistributing, or sublicensing any prompts, context, or other instructional content bundled within the Software
|
||||||
|
- Creating, distributing, or selling modified versions, forks, or Derivative Works of the Software
|
||||||
|
|
||||||
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
|
Monetization does NOT include:
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and modification follow.
|
- Using the Software internally within your organization, regardless of whether your organization is for-profit
|
||||||
|
- Using the Software to build products or services that generate revenue, as long as you are not reselling or redistributing the Software itself
|
||||||
|
- Using the Software to provide services for which fees are charged, as long as the Software itself is not being resold or redistributed
|
||||||
|
- Hosting the Software anywhere for personal use by a single developer, as long as the Software is not made accessible to others
|
||||||
|
|
||||||
TERMS AND CONDITIONS 0. Definitions.
|
"Core Contributors" means the following individuals who are granted perpetual, royalty-free licenses:
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
- Cody Seibert (webdevcody)
|
||||||
|
- SuperComboGamer (SCG)
|
||||||
|
- Kacper Lachowicz (Shironex, Shirone)
|
||||||
|
- Ben Scott (trueheads)
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
|
2. GRANT OF LICENSE
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
|
Subject to the terms and conditions of this Agreement, Licensor hereby grants to Licensee a non-exclusive, non-transferable license to use, copy, modify, and distribute the Software, provided that:
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based on the Program.
|
a) Licensee may freely clone, install, and use the Software locally or within an organization for the purpose of building, developing, and maintaining other products, software, or services. There are no restrictions on the products you build _using_ the Software.
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
|
b) Licensee may run the Software on personal or organizational infrastructure for internal use.
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
|
c) Core Contributors are each individually granted a perpetual, worldwide, royalty-free, non-exclusive license to use, copy, modify, distribute, and sublicense the Software for any purpose, including Monetization, without payment of any fees or royalties. Each Core Contributor may exercise these rights independently and does not require permission, consent, or approval from any other Core Contributor to Monetize the Software in any way they see fit.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
d) Commercial licenses for the Software may be discussed and issued to external parties or companies seeking to use the Software for financial gain or Monetization purposes. Core Contributors already have full rights under section 2(c) and do not require commercial licenses. Any commercial license issued to external parties shall require a unanimous vote by all Core Contributors and shall be granted in writing and signed by all Core Contributors.
|
||||||
|
|
||||||
1. Source Code.
|
e) The list of individuals defined as "Core Contributors" in Section 1 shall be amended to reflect any revocation or reinstatement of status made under this section.
|
||||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
3. RESTRICTIONS
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
Licensee may NOT:
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
|
- Engage in any Monetization of the Software or any Derivative Work without explicit written permission from all Core Contributors
|
||||||
|
- Resell, redistribute, or sublicense the Software, any Derivative Work, or any substantial portion thereof
|
||||||
|
- Create, distribute, or sell modified versions, forks, or Derivative Works of the Software for any commercial purpose
|
||||||
|
- Include the Software, any Derivative Work, or substantial portions thereof in a product or service that you sell or distribute
|
||||||
|
- Offer the Software, any Derivative Work, or substantial portions thereof as a standalone product or service for sale
|
||||||
|
- Extract, resell, redistribute, or sublicense any prompts, context, or other instructional content bundled within the Software
|
||||||
|
- Host the Software or any Derivative Work as a service (whether free or paid) for use by others (except Core Contributors)
|
||||||
|
- Remove or alter any copyright notices or license terms
|
||||||
|
- Use the Software in any manner that violates applicable laws or regulations
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
|
Licensee MAY:
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that same work.
|
- Use the Software internally within their organization (commercial or non-profit)
|
||||||
|
- Use the Software to build other commercial products (products that do NOT contain the Software or Derivative Works)
|
||||||
|
- Modify the Software for internal use within their organization (commercial or non-profit)
|
||||||
|
|
||||||
2. Basic Permissions.
|
4. CORE CONTRIBUTOR STATUS MANAGEMENT
|
||||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
a) Core Contributor status may be revoked indefinitely by the remaining Core Contributors if:
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
- A Core Contributor cannot be reached for a period of one (1) month through reasonable means of communication (including but not limited to email, Discord, GitHub, or other project communication channels)
|
||||||
|
- AND the Core Contributor has not contributed to the project during that one-month period. For purposes of this section, "contributed" means at least one of the following activities:
|
||||||
|
- Discussing the Software through project communication channels
|
||||||
|
- Committing code changes to the project repository
|
||||||
|
- Submitting bug fixes or patches
|
||||||
|
- Participating in project-related discussions or decision-making
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
b) Revocation of Core Contributor status requires a unanimous vote by all other Core Contributors (excluding the Core Contributor whose status is being considered for revocation).
|
||||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
|
c) Upon revocation of Core Contributor status, the individual shall no longer be considered a Core Contributor and shall lose the rights granted under section 2(c) of this Agreement. However, any Contributions made prior to revocation shall remain subject to the terms of section 5 (CONTRIBUTIONS AND RIGHTS ASSIGNMENT).
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
d) A revoked Core Contributor may be reinstated to Core Contributor status with a unanimous vote by all current Core Contributors. Upon reinstatement, the individual shall regain all rights granted under section 2(c) of this Agreement.
|
||||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
|
5. CONTRIBUTIONS AND RIGHTS ASSIGNMENT
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
By submitting, pushing, or contributing any code, documentation, pull requests, issues, or other materials ("Contributions") to the Automaker project, you agree to the following terms without reservation:
|
||||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
|
a) **Full Ownership Transfer & Rights Grant:** You hereby assign to the Core Contributors all right, title, and interest in and to your Contributions, including all copyrights, patents, and other intellectual property rights. If such assignment is not effective under applicable law, you grant the Core Contributors an unrestricted, perpetual, worldwide, non-exclusive, royalty-free, fully paid-up, irrevocable, sublicensable, and transferable license to use, reproduce, modify, adapt, publish, translate, create derivative works from, distribute, perform, display, and otherwise exploit your Contributions in any manner they see fit, including for any commercial purpose or Monetization.
|
||||||
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
|
|
||||||
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
|
|
||||||
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
|
|
||||||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
b) **No Take-Backs:** You understand and agree that this grant of rights is irrevocable ("no take-backs"). You cannot revoke, rescind, or terminate this grant of rights once your Contribution has been submitted.
|
||||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
|
c) **Waiver of Moral Rights:** You waive any "moral rights" or other rights with respect to attribution of authorship or integrity of materials regarding your Contributions that you may have under any applicable law.
|
||||||
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
|
|
||||||
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
|
|
||||||
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
|
|
||||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
|
d) **Right to Contribute:** You represent and warrant that you are the original author of the Contributions, or that you have sufficient rights to grant the rights conveyed by this section, and that your Contributions do not infringe upon the rights of any third party.
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
|
6. TERMINATION
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
|
This license will terminate automatically if Licensee breaches any term of this Agreement. Upon termination, Licensee must immediately cease all use of the Software and destroy all copies in their possession.
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
|
7. HIGH RISK DISCLAIMER AND LIMITATION OF LIABILITY
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
|
a) **AI RISKS:** THE SOFTWARE UTILIZES ARTIFICIAL INTELLIGENCE TO GENERATE CODE, EXECUTE COMMANDS, AND INTERACT WITH YOUR FILE SYSTEM. YOU ACKNOWLEDGE THAT AI SYSTEMS CAN BE UNPREDICTABLE, MAY GENERATE INCORRECT, INSECURE, OR DESTRUCTIVE CODE, AND MAY TAKE ACTIONS THAT COULD DAMAGE YOUR SYSTEM, FILES, OR HARDWARE.
|
||||||
|
|
||||||
7. Additional Terms.
|
b) **USE AT YOUR OWN RISK:** YOU AGREE THAT YOUR USE OF THE SOFTWARE IS SOLELY AT YOUR OWN RISK. THE CORE CONTRIBUTORS AND LICENSOR DO NOT GUARANTEE THAT THE SOFTWARE OR ANY CODE GENERATED BY IT WILL BE SAFE, BUG-FREE, OR FUNCTIONAL.
|
||||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
|
c) **NO WARRANTY:** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
|
d) **LIMITATION OF LIABILITY:** IN NO EVENT SHALL THE CORE CONTRIBUTORS, LICENSORS, OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE, INCLUDING BUT NOT LIMITED TO:
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
|
- DAMAGE TO HARDWARE OR COMPUTER SYSTEMS
|
||||||
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
|
- DATA LOSS OR CORRUPTION
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
|
- GENERATION OF BAD, VULNERABLE, OR MALICIOUS CODE
|
||||||
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
|
- FINANCIAL LOSSES
|
||||||
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
|
- BUSINESS INTERRUPTION
|
||||||
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
|
|
||||||
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
8. LICENSE AMENDMENTS
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
|
Any amendment, modification, or update to this License Agreement must be agreed upon unanimously by all Core Contributors. No changes to this Agreement shall be effective unless all Core Contributors have provided their written consent or approval through a unanimous vote.
|
||||||
|
|
||||||
8. Termination.
|
9. CONTACT
|
||||||
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
|
For inquiries regarding this license or permissions for Monetization, please contact the Core Contributors through the official project channels:
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
|
- Agentic Jumpstart Discord: https://discord.gg/JUDWZDN3VT
|
||||||
|
- Website: https://automaker.app
|
||||||
|
- Email: automakerapp@gmail.com
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
|
Any permission for Monetization requires the unanimous written consent of all Core Contributors.
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
10. GOVERNING LAW
|
||||||
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
This Agreement shall be governed by and construed in accordance with the laws of the State of Tennessee, USA, without regard to conflict of law principles.
|
||||||
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
|
By using the Software, you acknowledge that you have read this Agreement, understand it, and agree to be bound by its terms and conditions.
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
|
---
|
||||||
|
|
||||||
11. Patents.
|
Copyright (c) 2025 Automaker Core Contributors
|
||||||
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|||||||
657
README.md
657
README.md
@@ -1,20 +1,99 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="apps/ui/public/readme_logo.svg" alt="Automaker Logo" height="80" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> **[!TIP]**
|
||||||
|
>
|
||||||
|
> **Learn more about Agentic Coding!**
|
||||||
|
>
|
||||||
|
> Automaker itself was built by a group of engineers using AI and agentic coding techniques to build features faster than ever. By leveraging tools like Cursor IDE and Claude Code CLI, the team orchestrated AI agents to implement complex functionality in days instead of weeks.
|
||||||
|
>
|
||||||
|
> **Learn how:** Master these same techniques and workflows in the [Agentic Jumpstart course](https://agenticjumpstart.com/?utm=automaker-gh).
|
||||||
|
|
||||||
# Automaker
|
# Automaker
|
||||||
|
|
||||||
Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion.
|
**Stop typing code. Start directing AI agents.**
|
||||||
|
|
||||||
---
|
<details open>
|
||||||
|
<summary><h2>Table of Contents</h2></summary>
|
||||||
|
|
||||||
> **[!CAUTION]**
|
- [What Makes Automaker Different?](#what-makes-automaker-different)
|
||||||
>
|
- [The Workflow](#the-workflow)
|
||||||
> ## Security Disclaimer
|
- [Powered by Claude Agent SDK](#powered-by-claude-agent-sdk)
|
||||||
>
|
- [Why This Matters](#why-this-matters)
|
||||||
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
|
- [Security Disclaimer](#security-disclaimer)
|
||||||
>
|
- [Community & Support](#community--support)
|
||||||
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
|
- [Getting Started](#getting-started)
|
||||||
>
|
- [Prerequisites](#prerequisites)
|
||||||
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
|
- [Quick Start](#quick-start)
|
||||||
>
|
- [How to Run](#how-to-run)
|
||||||
> **[Read the full disclaimer](../DISCLAIMER.md)**
|
- [Development Mode](#development-mode)
|
||||||
|
- [Building for Production](#building-for-production)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Linting](#linting)
|
||||||
|
- [Environment Configuration](#environment-configuration)
|
||||||
|
- [Authentication Setup](#authentication-setup)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Core Workflow](#core-workflow)
|
||||||
|
- [AI & Planning](#ai--planning)
|
||||||
|
- [Project Management](#project-management)
|
||||||
|
- [Collaboration & Review](#collaboration--review)
|
||||||
|
- [Developer Tools](#developer-tools)
|
||||||
|
- [Advanced Features](#advanced-features)
|
||||||
|
- [Tech Stack](#tech-stack)
|
||||||
|
- [Frontend](#frontend)
|
||||||
|
- [Backend](#backend)
|
||||||
|
- [Testing & Quality](#testing--quality)
|
||||||
|
- [Shared Libraries](#shared-libraries)
|
||||||
|
- [Available Views](#available-views)
|
||||||
|
- [Architecture](#architecture)
|
||||||
|
- [Monorepo Structure](#monorepo-structure)
|
||||||
|
- [How It Works](#how-it-works)
|
||||||
|
- [Key Architectural Patterns](#key-architectural-patterns)
|
||||||
|
- [Security & Isolation](#security--isolation)
|
||||||
|
- [Data Storage](#data-storage)
|
||||||
|
- [Learn More](#learn-more)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Agent SDK automatically implement them. Built with React, Vite, Electron, and Express, Automaker provides a complete workflow for managing AI agents through a desktop application (or web browser), with features like real-time streaming, git worktree isolation, plan approval, and multi-agent task execution.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## What Makes Automaker Different?
|
||||||
|
|
||||||
|
Traditional development tools help you write code. Automaker helps you **orchestrate AI agents** to build entire features autonomously. Think of it as having a team of AI developers working for you—you define what needs to be built, and Automaker handles the implementation.
|
||||||
|
|
||||||
|
### The Workflow
|
||||||
|
|
||||||
|
1. **Add Features** - Describe features you want built (with text, images, or screenshots)
|
||||||
|
2. **Move to "In Progress"** - Automaker automatically assigns an AI agent to implement the feature
|
||||||
|
3. **Watch It Build** - See real-time progress as the agent writes code, runs tests, and makes changes
|
||||||
|
4. **Review & Verify** - Review the changes, run tests, and approve when ready
|
||||||
|
5. **Ship Faster** - Build entire applications in days, not weeks
|
||||||
|
|
||||||
|
### Powered by Claude Agent SDK
|
||||||
|
|
||||||
|
Automaker leverages the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commits—all while working in isolated git worktrees to keep your main branch safe. The SDK provides autonomous AI agents that can use tools, make decisions, and complete complex multi-step tasks without constant human intervention.
|
||||||
|
|
||||||
|
### Why This Matters
|
||||||
|
|
||||||
|
The future of software development is **agentic coding**—where developers become architects directing AI agents rather than manual coders. Automaker puts this future in your hands today, letting you experience what it's like to build software 10x faster with AI agents handling the implementation while you focus on architecture and business logic.
|
||||||
|
|
||||||
|
## Community & Support
|
||||||
|
|
||||||
|
Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows.
|
||||||
|
|
||||||
|
In the Discord, you can:
|
||||||
|
|
||||||
|
- 💬 Discuss agentic coding patterns and best practices
|
||||||
|
- 🧠 Share ideas for AI-driven development workflows
|
||||||
|
- 🛠️ Get help setting up or extending Automaker
|
||||||
|
- 🚀 Show off projects built with AI agents
|
||||||
|
- 🤝 Collaborate with other developers and contributors
|
||||||
|
|
||||||
|
👉 **Join the Discord:** [Agentic Jumpstart Discord](https://discord.gg/jjem7aEDKU)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,34 +101,68 @@ Automaker is an autonomous AI development studio that helps you build software f
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js 18+
|
- **Node.js 18+** (tested with Node.js 22)
|
||||||
- npm
|
- **npm** (comes with Node.js)
|
||||||
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
|
- **Authentication** (choose one):
|
||||||
|
- **[Claude Code CLI](https://code.claude.com/docs/en/overview)** (recommended) - Install and authenticate, credentials used automatically
|
||||||
|
- **Anthropic API Key** - Direct API key for Claude Agent SDK ([get one here](https://console.anthropic.com/))
|
||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone the repo
|
# 1. Clone the repository
|
||||||
git clone git@github.com:AutoMaker-Org/automaker.git
|
git clone https://github.com/AutoMaker-Org/automaker.git
|
||||||
cd automaker
|
cd automaker
|
||||||
|
|
||||||
# 2. Install dependencies
|
# 2. Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 3. Get your Claude Code OAuth token
|
# 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly)
|
||||||
claude setup-token
|
npm run build:packages
|
||||||
# ⚠️ This prints your token - don't share your screen!
|
|
||||||
|
|
||||||
# 4. Set the token and run
|
# 4. Start Automaker (production mode)
|
||||||
export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat01-..."
|
npm run start
|
||||||
npm run dev:electron
|
# Choose between:
|
||||||
|
# 1. Web Application (browser at localhost:3007)
|
||||||
|
# 2. Desktop Application (Electron - recommended)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** The `npm run start` command will:
|
||||||
|
|
||||||
|
- Check for dependencies and install if needed
|
||||||
|
- Build the application if needed
|
||||||
|
- Kill any processes on ports 3007/3008
|
||||||
|
- Present an interactive menu to choose your run mode
|
||||||
|
- Run in production mode (no hot reload)
|
||||||
|
|
||||||
|
**Authentication Setup:** On first run, Automaker will automatically show a setup wizard where you can configure authentication. You can choose to:
|
||||||
|
|
||||||
|
- Use **Claude Code CLI** (recommended) - Automaker will detect your CLI credentials automatically
|
||||||
|
- Enter an **API key** directly in the wizard
|
||||||
|
|
||||||
|
If you prefer to set up authentication before running (e.g., for headless deployments or CI/CD), you can set it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option A: Environment variable
|
||||||
|
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||||
|
|
||||||
|
# Option B: Create .env file in project root
|
||||||
|
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Development:** If you want to develop on Automaker with Vite live reload and hot module replacement, use `npm run dev` instead. This will start the development server with fast refresh and instant updates as you make changes.
|
||||||
|
|
||||||
## How to Run
|
## How to Run
|
||||||
|
|
||||||
### Development Modes
|
### Development Mode
|
||||||
|
|
||||||
Automaker can be run in several modes:
|
Start Automaker in development mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will prompt you to choose your run mode, or you can specify a mode directly:
|
||||||
|
|
||||||
#### Electron Desktop App (Recommended)
|
#### Electron Desktop App (Recommended)
|
||||||
|
|
||||||
@@ -72,37 +185,166 @@ npm run dev:electron:wsl:gpu
|
|||||||
```bash
|
```bash
|
||||||
# Run in web browser (http://localhost:3007)
|
# Run in web browser (http://localhost:3007)
|
||||||
npm run dev:web
|
npm run dev:web
|
||||||
# or
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building for Production
|
### Building for Production
|
||||||
|
|
||||||
|
#### Web Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build Next.js app
|
# Build for web deployment (uses Vite)
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Build Electron app for distribution
|
# Run production build
|
||||||
npm run build:electron
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Production Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start production Next.js server
|
|
||||||
npm run start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
#### Desktop Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tests headless
|
# Build for current platform (macOS/Windows/Linux)
|
||||||
npm run test
|
npm run build:electron
|
||||||
|
|
||||||
# Run tests with browser visible
|
# Platform-specific builds
|
||||||
npm run test:headed
|
npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64)
|
||||||
|
npm run build:electron:win # Windows (NSIS installer, x64)
|
||||||
|
npm run build:electron:linux # Linux (AppImage + DEB, x64)
|
||||||
|
|
||||||
|
# Output directory: apps/ui/release/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Docker Deployment
|
||||||
|
|
||||||
|
Docker provides the most secure way to run Automaker by isolating it from your host filesystem.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and run with Docker Compose
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Access UI at http://localhost:3007
|
||||||
|
# API at http://localhost:3008
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop containers
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Configuration
|
||||||
|
|
||||||
|
Create a `.env` file in the project root if using API key authentication:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Optional: Anthropic API key (not needed if using Claude CLI authentication)
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Most users authenticate via Claude CLI instead of API keys. See [Claude CLI Authentication](#claude-cli-authentication-optional) below.
|
||||||
|
|
||||||
|
##### Working with Projects (Host Directory Access)
|
||||||
|
|
||||||
|
By default, the container is isolated from your host filesystem. To work on projects from your host machine, create a `docker-compose.override.yml` file (gitignored):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
volumes:
|
||||||
|
# Mount your project directories
|
||||||
|
- /path/to/your/project:/projects/your-project
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Claude CLI Authentication (Optional)
|
||||||
|
|
||||||
|
To use Claude Code CLI authentication instead of an API key, mount your Claude CLI config directory:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
volumes:
|
||||||
|
# Linux/macOS
|
||||||
|
- ~/.claude:/home/automaker/.claude
|
||||||
|
# Windows
|
||||||
|
- C:/Users/YourName/.claude:/home/automaker/.claude
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The Claude CLI config must be writable (do not use `:ro` flag) as the CLI writes debug files.
|
||||||
|
|
||||||
|
##### GitHub CLI Authentication (For Git Push/PR Operations)
|
||||||
|
|
||||||
|
To enable git push and GitHub CLI operations inside the container:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
volumes:
|
||||||
|
# Mount GitHub CLI config
|
||||||
|
# Linux/macOS
|
||||||
|
- ~/.config/gh:/home/automaker/.config/gh
|
||||||
|
# Windows
|
||||||
|
- 'C:/Users/YourName/AppData/Roaming/GitHub CLI:/home/automaker/.config/gh'
|
||||||
|
|
||||||
|
# Mount git config for user identity (name, email)
|
||||||
|
- ~/.gitconfig:/home/automaker/.gitconfig:ro
|
||||||
|
environment:
|
||||||
|
# GitHub token (required on Windows where tokens are in Credential Manager)
|
||||||
|
# Get your token with: gh auth token
|
||||||
|
- GH_TOKEN=${GH_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add `GH_TOKEN` to your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GH_TOKEN=gho_your_github_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Complete docker-compose.override.yml Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
volumes:
|
||||||
|
# Your projects
|
||||||
|
- /path/to/project1:/projects/project1
|
||||||
|
- /path/to/project2:/projects/project2
|
||||||
|
|
||||||
|
# Authentication configs
|
||||||
|
- ~/.claude:/home/automaker/.claude
|
||||||
|
- ~/.config/gh:/home/automaker/.config/gh
|
||||||
|
- ~/.gitconfig:/home/automaker/.gitconfig:ro
|
||||||
|
environment:
|
||||||
|
- GH_TOKEN=${GH_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Architecture Support
|
||||||
|
|
||||||
|
The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
#### End-to-End Tests (Playwright)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test # Headless E2E tests
|
||||||
|
npm run test:headed # Browser visible E2E tests
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Unit Tests (Vitest)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:server # Server unit tests
|
||||||
|
npm run test:server:coverage # Server tests with coverage
|
||||||
|
npm run test:packages # All shared package tests
|
||||||
|
npm run test:all # Packages + server tests
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test Configuration
|
||||||
|
|
||||||
|
- E2E tests run on ports 3007 (UI) and 3008 (server)
|
||||||
|
- Automatically starts test servers before running
|
||||||
|
- Uses Chromium browser via Playwright
|
||||||
|
- Mock agent mode available in CI with `AUTOMAKER_MOCK_AGENT=true`
|
||||||
|
|
||||||
### Linting
|
### Linting
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -110,64 +352,323 @@ npm run test:headed
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authentication Options
|
### Environment Configuration
|
||||||
|
|
||||||
Automaker supports multiple authentication methods (in order of priority):
|
#### Authentication (if not using Claude Code CLI)
|
||||||
|
|
||||||
| Method | Environment Variable | Description |
|
- `ANTHROPIC_API_KEY` - Your Anthropic API key for Claude Agent SDK (not needed if using Claude Code CLI)
|
||||||
| -------------------- | ------------------------- | --------------------------------------------------------- |
|
|
||||||
| OAuth Token (env) | `CLAUDE_CODE_OAUTH_TOKEN` | From `claude setup-token` - uses your Claude subscription |
|
|
||||||
| OAuth Token (stored) | — | Stored in app credentials file |
|
|
||||||
| API Key (stored) | — | Anthropic API key stored in app |
|
|
||||||
| API Key (env) | `ANTHROPIC_API_KEY` | Pay-per-use API key |
|
|
||||||
|
|
||||||
**Recommended:** Use `CLAUDE_CODE_OAUTH_TOKEN` if you have a Claude subscription.
|
#### Optional - Server
|
||||||
|
|
||||||
### Persistent Setup (Optional)
|
- `PORT` - Server port (default: 3008)
|
||||||
|
- `DATA_DIR` - Data storage directory (default: ./data)
|
||||||
|
- `ENABLE_REQUEST_LOGGING` - HTTP request logging (default: true)
|
||||||
|
|
||||||
|
#### Optional - Security
|
||||||
|
|
||||||
|
- `AUTOMAKER_API_KEY` - Optional API authentication for the server
|
||||||
|
- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory
|
||||||
|
- `CORS_ORIGIN` - CORS policy (default: \*)
|
||||||
|
|
||||||
|
#### Optional - Development
|
||||||
|
|
||||||
|
- `VITE_SKIP_ELECTRON` - Skip Electron in dev mode
|
||||||
|
- `OPEN_DEVTOOLS` - Auto-open DevTools in Electron
|
||||||
|
- `ENABLE_DEBUG_PANEL` - Enable the debug panel in non-development builds (for staging environments)
|
||||||
|
|
||||||
|
### Authentication Setup
|
||||||
|
|
||||||
|
#### Option 1: Claude Code CLI (Recommended)
|
||||||
|
|
||||||
|
Install and authenticate the Claude Code CLI following the [official quickstart guide](https://code.claude.com/docs/en/quickstart).
|
||||||
|
|
||||||
|
Once authenticated, Automaker will automatically detect and use your CLI credentials. No additional configuration needed!
|
||||||
|
|
||||||
|
#### Option 2: Direct API Key
|
||||||
|
|
||||||
|
If you prefer not to use the CLI, you can provide an Anthropic API key directly using one of these methods:
|
||||||
|
|
||||||
|
##### 2a. Shell Configuration
|
||||||
|
|
||||||
Add to your `~/.bashrc` or `~/.zshrc`:
|
Add to your `~/.bashrc` or `~/.zshrc`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export CLAUDE_CODE_OAUTH_TOKEN="YOUR_TOKEN_HERE"
|
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||||
```
|
```
|
||||||
|
|
||||||
Then restart your terminal or run `source ~/.bashrc`.
|
Then restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc`).
|
||||||
|
|
||||||
|
##### 2b. .env File
|
||||||
|
|
||||||
|
Create a `.env` file in the project root (gitignored):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
PORT=3008
|
||||||
|
DATA_DIR=./data
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2c. In-App Storage
|
||||||
|
|
||||||
|
The application can store your API key securely in the settings UI. The key is persisted in the `DATA_DIR` directory.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
### Core Workflow
|
||||||
|
|
||||||
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
|
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
|
||||||
- 🤖 **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress"
|
- 🤖 **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress"
|
||||||
- 🧠 **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more
|
- 🔀 **Git Worktree Isolation** - Each feature executes in isolated git worktrees to protect your main branch
|
||||||
- 💭 **Extended Thinking** - Enable extended thinking modes for complex problem-solving
|
- 📡 **Real-time Streaming** - Watch AI agents work in real-time with live tool usage, progress updates, and task completion
|
||||||
- 📡 **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented
|
- 🔄 **Follow-up Instructions** - Send additional instructions to running agents without stopping them
|
||||||
- 🔍 **Project Analysis** - AI-powered project structure analysis to understand your codebase
|
|
||||||
- 📁 **Context Management** - Add context files to help AI agents understand your project better
|
### AI & Planning
|
||||||
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on your project
|
|
||||||
- 🖼️ **Image Support** - Attach images and screenshots to feature descriptions
|
- 🧠 **Multi-Model Support** - Choose from Claude Opus, Sonnet, and Haiku per feature
|
||||||
- ⚡ **Concurrent Processing** - Configure concurrency to process multiple features simultaneously
|
- 💭 **Extended Thinking** - Enable thinking modes (none, medium, deep, ultra) for complex problem-solving
|
||||||
- 🧪 **Test Integration** - Automatic test running and verification for implemented features
|
- 📝 **Planning Modes** - Four planning levels: skip (direct implementation), lite (quick plan), spec (task breakdown), full (phased execution)
|
||||||
- 🔀 **Git Integration** - View git diffs and track changes made by AI agents
|
- ✅ **Plan Approval** - Review and approve AI-generated plans before implementation begins
|
||||||
- 👤 **AI Profiles** - Create and manage different AI agent profiles for various tasks
|
- 📊 **Multi-Agent Task Execution** - Spec mode spawns dedicated agents per task for focused implementation
|
||||||
- 💬 **Chat History** - Keep track of conversations and interactions with AI agents
|
|
||||||
- ⌨️ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts
|
### Project Management
|
||||||
- 🎨 **Dark/Light Theme** - Beautiful UI with theme support
|
|
||||||
- 🖥️ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux
|
- 🔍 **Project Analysis** - AI-powered codebase analysis to understand your project structure
|
||||||
|
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on project analysis
|
||||||
|
- 📁 **Context Management** - Add markdown, images, and documentation files that agents automatically reference
|
||||||
|
- 🔗 **Dependency Blocking** - Features can depend on other features, enforcing execution order
|
||||||
|
- 🌳 **Graph View** - Visualize feature dependencies with interactive graph visualization
|
||||||
|
- 📋 **GitHub Integration** - Import issues, validate feasibility, and convert to tasks automatically
|
||||||
|
|
||||||
|
### Collaboration & Review
|
||||||
|
|
||||||
|
- 🧪 **Verification Workflow** - Features move to "Waiting Approval" for review and testing
|
||||||
|
- 💬 **Agent Chat** - Interactive chat sessions with AI agents for exploratory work
|
||||||
|
- 👤 **AI Profiles** - Create custom agent configurations with different prompts, models, and settings
|
||||||
|
- 📜 **Session History** - Persistent chat sessions across restarts with full conversation history
|
||||||
|
- 🔍 **Git Diff Viewer** - Review changes made by agents before approving
|
||||||
|
|
||||||
|
### Developer Tools
|
||||||
|
|
||||||
|
- 🖥️ **Integrated Terminal** - Full terminal access with tabs, splits, and persistent sessions
|
||||||
|
- 🖼️ **Image Support** - Attach screenshots and diagrams to feature descriptions for visual context
|
||||||
|
- ⚡ **Concurrent Execution** - Configure how many features can run simultaneously (default: 3)
|
||||||
|
- ⌨️ **Keyboard Shortcuts** - Fully customizable shortcuts for navigation and actions
|
||||||
|
- 🎨 **Theme System** - 25+ themes including Dark, Light, Dracula, Nord, Catppuccin, and more
|
||||||
|
- 🖥️ **Cross-Platform** - Desktop app for macOS (x64, arm64), Windows (x64), and Linux (x64)
|
||||||
|
- 🌐 **Web Mode** - Run in browser or as Electron desktop app
|
||||||
|
- 🐛 **Debug Panel** - Floating debug overlay for monitoring memory, CPU, and process performance (dev mode only, toggle with `Cmd/Ctrl+Shift+D`)
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
|
||||||
|
- 🔐 **Docker Isolation** - Security-focused Docker deployment with no host filesystem access
|
||||||
|
- 🎯 **Worktree Management** - Create, switch, commit, and create PRs from worktrees
|
||||||
|
- 📊 **Usage Tracking** - Monitor Claude API usage with detailed metrics
|
||||||
|
- 🔊 **Audio Notifications** - Optional completion sounds (mutable in settings)
|
||||||
|
- 💾 **Auto-save** - All work automatically persisted to `.automaker/` directory
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org) - React framework
|
### Frontend
|
||||||
- [Electron](https://www.electronjs.org/) - Desktop application framework
|
|
||||||
- [Tailwind CSS](https://tailwindcss.com/) - Styling
|
- **React 19** - UI framework
|
||||||
- [Zustand](https://zustand-demo.pmnd.rs/) - State management
|
- **Vite 7** - Build tool and development server
|
||||||
- [dnd-kit](https://dndkit.com/) - Drag and drop functionality
|
- **Electron 39** - Desktop application framework
|
||||||
|
- **TypeScript 5.9** - Type safety
|
||||||
|
- **TanStack Router** - File-based routing
|
||||||
|
- **Zustand 5** - State management with persistence
|
||||||
|
- **Tailwind CSS 4** - Utility-first styling with 25+ themes
|
||||||
|
- **Radix UI** - Accessible component primitives
|
||||||
|
- **dnd-kit** - Drag and drop for Kanban board
|
||||||
|
- **@xyflow/react** - Graph visualization for dependencies
|
||||||
|
- **xterm.js** - Integrated terminal emulator
|
||||||
|
- **CodeMirror 6** - Code editor for XML/syntax highlighting
|
||||||
|
- **Lucide Icons** - Icon library
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- **Node.js** - JavaScript runtime with ES modules
|
||||||
|
- **Express 5** - HTTP server framework
|
||||||
|
- **TypeScript 5.9** - Type safety
|
||||||
|
- **Claude Agent SDK** - AI agent integration (@anthropic-ai/claude-agent-sdk)
|
||||||
|
- **WebSocket (ws)** - Real-time event streaming
|
||||||
|
- **node-pty** - PTY terminal sessions
|
||||||
|
|
||||||
|
### Testing & Quality
|
||||||
|
|
||||||
|
- **Playwright** - End-to-end testing
|
||||||
|
- **Vitest** - Unit testing framework
|
||||||
|
- **ESLint 9** - Code linting
|
||||||
|
- **Prettier 3** - Code formatting
|
||||||
|
- **Husky** - Git hooks for pre-commit formatting
|
||||||
|
|
||||||
|
### Shared Libraries
|
||||||
|
|
||||||
|
- **@automaker/types** - Shared TypeScript definitions
|
||||||
|
- **@automaker/utils** - Logging, error handling, image processing
|
||||||
|
- **@automaker/prompts** - AI prompt templates
|
||||||
|
- **@automaker/platform** - Path management and security
|
||||||
|
- **@automaker/model-resolver** - Claude model alias resolution
|
||||||
|
- **@automaker/dependency-resolver** - Feature dependency ordering
|
||||||
|
- **@automaker/git-utils** - Git operations and worktree management
|
||||||
|
|
||||||
|
## Available Views
|
||||||
|
|
||||||
|
Automaker provides several specialized views accessible via the sidebar or keyboard shortcuts:
|
||||||
|
|
||||||
|
| View | Shortcut | Description |
|
||||||
|
| ------------------ | -------- | ------------------------------------------------------------------------------------------------ |
|
||||||
|
| **Board** | `K` | Kanban board for managing feature workflow (Backlog → In Progress → Waiting Approval → Verified) |
|
||||||
|
| **Agent** | `A` | Interactive chat sessions with AI agents for exploratory work and questions |
|
||||||
|
| **Spec** | `D` | Project specification editor with AI-powered generation and feature suggestions |
|
||||||
|
| **Context** | `C` | Manage context files (markdown, images) that AI agents automatically reference |
|
||||||
|
| **Profiles** | `M` | Create and manage AI agent profiles with custom prompts and configurations |
|
||||||
|
| **Settings** | `S` | Configure themes, shortcuts, defaults, authentication, and more |
|
||||||
|
| **Terminal** | `T` | Integrated terminal with tabs, splits, and persistent sessions |
|
||||||
|
| **GitHub Issues** | - | Import and validate GitHub issues, convert to tasks |
|
||||||
|
| **Running Agents** | - | View all active agents across projects with status and progress |
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
All shortcuts are customizable in Settings. Default shortcuts:
|
||||||
|
|
||||||
|
- **Navigation:** `K` (Board), `A` (Agent), `D` (Spec), `C` (Context), `S` (Settings), `M` (Profiles), `T` (Terminal)
|
||||||
|
- **UI:** `` ` `` (Toggle sidebar)
|
||||||
|
- **Actions:** `N` (New item in current view), `G` (Start next features), `O` (Open project), `P` (Project picker)
|
||||||
|
- **Projects:** `Q`/`E` (Cycle previous/next project)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
|
||||||
|
Automaker is built as an npm workspace monorepo with two main applications and seven shared packages:
|
||||||
|
|
||||||
|
```text
|
||||||
|
automaker/
|
||||||
|
├── apps/
|
||||||
|
│ ├── ui/ # React + Vite + Electron frontend
|
||||||
|
│ └── server/ # Express + WebSocket backend
|
||||||
|
└── libs/ # Shared packages
|
||||||
|
├── types/ # Core TypeScript definitions
|
||||||
|
├── utils/ # Logging, errors, utilities
|
||||||
|
├── prompts/ # AI prompt templates
|
||||||
|
├── platform/ # Path management, security
|
||||||
|
├── model-resolver/ # Claude model aliasing
|
||||||
|
├── dependency-resolver/ # Feature dependency ordering
|
||||||
|
└── git-utils/ # Git operations & worktree management
|
||||||
|
```
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Feature Definition** - Users create feature cards on the Kanban board with descriptions, images, and configuration
|
||||||
|
2. **Git Worktree Creation** - When a feature starts, a git worktree is created for isolated development
|
||||||
|
3. **Agent Execution** - Claude Agent SDK executes in the worktree with full file system and command access
|
||||||
|
4. **Real-time Streaming** - Agent output streams via WebSocket to the frontend for live monitoring
|
||||||
|
5. **Plan Approval** (optional) - For spec/full planning modes, agents generate plans that require user approval
|
||||||
|
6. **Multi-Agent Tasks** (spec mode) - Each task in the spec gets a dedicated agent for focused implementation
|
||||||
|
7. **Verification** - Features move to "Waiting Approval" where changes can be reviewed via git diff
|
||||||
|
8. **Integration** - After approval, changes can be committed and PRs created from the worktree
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
- **Event-Driven Architecture** - All server operations emit events that stream to the frontend
|
||||||
|
- **Provider Pattern** - Extensible AI provider system (currently Claude, designed for future providers)
|
||||||
|
- **Service-Oriented Backend** - Modular services for agent management, features, terminals, settings
|
||||||
|
- **State Management** - Zustand with persistence for frontend state across restarts
|
||||||
|
- **File-Based Storage** - No database; features stored as JSON files in `.automaker/` directory
|
||||||
|
|
||||||
|
### Security & Isolation
|
||||||
|
|
||||||
|
- **Git Worktrees** - Each feature executes in an isolated git worktree, protecting your main branch
|
||||||
|
- **Path Sandboxing** - Optional `ALLOWED_ROOT_DIRECTORY` restricts file access
|
||||||
|
- **Docker Isolation** - Recommended deployment uses Docker with no host filesystem access
|
||||||
|
- **Plan Approval** - Optional plan review before implementation prevents unwanted changes
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
|
||||||
|
Automaker uses a file-based storage system (no database required):
|
||||||
|
|
||||||
|
#### Per-Project Data
|
||||||
|
|
||||||
|
Stored in `{projectPath}/.automaker/`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.automaker/
|
||||||
|
├── features/ # Feature JSON files and images
|
||||||
|
│ └── {featureId}/
|
||||||
|
│ ├── feature.json # Feature metadata
|
||||||
|
│ ├── agent-output.md # AI agent output log
|
||||||
|
│ └── images/ # Attached images
|
||||||
|
├── context/ # Context files for AI agents
|
||||||
|
├── settings.json # Project-specific settings
|
||||||
|
├── spec.md # Project specification
|
||||||
|
├── analysis.json # Project structure analysis
|
||||||
|
└── feature-suggestions.json # AI-generated suggestions
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Global Data
|
||||||
|
|
||||||
|
Stored in `DATA_DIR` (default `./data`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/
|
||||||
|
├── settings.json # Global settings, profiles, shortcuts
|
||||||
|
├── credentials.json # API keys (encrypted)
|
||||||
|
├── sessions-metadata.json # Chat session metadata
|
||||||
|
└── agent-sessions/ # Conversation histories
|
||||||
|
└── {sessionId}.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **[!CAUTION]**
|
||||||
|
>
|
||||||
|
> ## Security Disclaimer
|
||||||
|
>
|
||||||
|
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
|
||||||
|
>
|
||||||
|
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
|
||||||
|
>
|
||||||
|
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
|
||||||
|
>
|
||||||
|
> **[Read the full disclaimer](./DISCLAIMER.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
### Documentation
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
- [Contributing Guide](./CONTRIBUTING.md) - How to contribute to Automaker
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
- [Project Documentation](./docs/) - Architecture guides, patterns, and developer docs
|
||||||
|
- [Docker Isolation Guide](./docs/docker-isolation.md) - Security-focused Docker deployment
|
||||||
|
- [Shared Packages Guide](./docs/llm-shared-packages.md) - Using monorepo packages
|
||||||
|
|
||||||
|
### Community
|
||||||
|
|
||||||
|
Join the **Agentic Jumpstart** Discord to connect with other builders exploring **agentic coding**:
|
||||||
|
|
||||||
|
👉 [Agentic Jumpstart Discord](https://discord.gg/jjem7aEDKU)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
See [LICENSE](../LICENSE) for details.
|
This project is licensed under the **Automaker License Agreement**. See [LICENSE](LICENSE) for the full text.
|
||||||
|
|
||||||
|
**Summary of Terms:**
|
||||||
|
|
||||||
|
- **Allowed:**
|
||||||
|
- **Build Anything:** You can clone and use Automaker locally or in your organization to build ANY product (commercial or free).
|
||||||
|
- **Internal Use:** You can use it internally within your company (commercial or non-profit) without restriction.
|
||||||
|
- **Modify:** You can modify the code for internal use within your organization (commercial or non-profit).
|
||||||
|
|
||||||
|
- **Restricted (The "No Monetization of the Tool" Rule):**
|
||||||
|
- **No Resale:** You cannot resell Automaker itself.
|
||||||
|
- **No SaaS:** You cannot host Automaker as a service for others.
|
||||||
|
- **No Monetizing Mods:** You cannot distribute modified versions of Automaker for money.
|
||||||
|
|
||||||
|
- **Liability:**
|
||||||
|
- **Use at Own Risk:** This tool uses AI. We are **NOT** responsible if it breaks your computer, deletes your files, or generates bad code. You assume all risk.
|
||||||
|
|
||||||
|
- **Contributing:**
|
||||||
|
- By contributing to this repository, you grant the Core Contributors full, irrevocable rights to your code (copyright assignment).
|
||||||
|
|
||||||
|
**Core Contributors** (Cody Seibert (webdevcody), SuperComboGamer (SCG), Kacper Lachowicz (Shironex, Shirone), and Ben Scott (trueheads)) are granted perpetual, royalty-free licenses for any use, including monetization.
|
||||||
|
|||||||
17
TODO.md
Normal file
17
TODO.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Bugs
|
||||||
|
|
||||||
|
- Setting the default model does not seem like it works.
|
||||||
|
|
||||||
|
# UX
|
||||||
|
|
||||||
|
- Consolidate all models to a single place in the settings instead of having AI profiles and all this other stuff
|
||||||
|
- Simplify the create feature modal. It should just be one page. I don't need nessa tabs and all these nested buttons. It's too complex.
|
||||||
|
- added to do's list checkbox directly into the card so as it's going through if there's any to do items we can see those update live
|
||||||
|
- When the feature is done, I want to see a summary of the LLM. That's the first thing I should see when I double click the card.
|
||||||
|
- I went away to mass edit all my features. For example, when I created a new project, it added auto testing on every single feature card. Now I have to manually go through one by one and change those. Have a way to mass edit those, the configuration of all them.
|
||||||
|
- Double check and debug if there's memory leaks. It seems like the memory of automaker grows like 3 gigabytes. It's 5gb right now and I'm running three different cursor cli features implementing at the same time.
|
||||||
|
- Typing in the text area of the plan mode was super laggy.
|
||||||
|
- When I have a bunch of features running at the same time, it seems like I cannot edit the features in the backlog. Like they don't persist their file changes and I think this is because of the secure FS file has an internal queue to prevent hitting that file open write limit. We may have to reconsider refactoring away from file system and do Postgres or SQLite or something.
|
||||||
|
- modals are not scrollable if height of the screen is small enough
|
||||||
|
- and the Agent Runner add an archival button for the new sessions.
|
||||||
|
- investigate a potential issue with the feature cards not refreshing. I see a lock icon on the feature card But it doesn't go away until I open the card and edit it and I turn the testing mode off. I think there's like a refresh sync issue.
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# Automaker
|
|
||||||
|
|
||||||
Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **[!CAUTION]**
|
|
||||||
>
|
|
||||||
> ## Security Disclaimer
|
|
||||||
>
|
|
||||||
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
|
|
||||||
>
|
|
||||||
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
|
|
||||||
>
|
|
||||||
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
|
|
||||||
>
|
|
||||||
> **[Read the full disclaimer](../DISCLAIMER.md)**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
**Step 1:** Clone this repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone git@github.com:AutoMaker-Org/automaker.git
|
|
||||||
cd automaker
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2:** Install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows notes (in-app Claude auth)
|
|
||||||
|
|
||||||
- Node.js 22.x
|
|
||||||
- Prebuilt PTY is bundled; Visual Studio build tools are not required for Claude auth.
|
|
||||||
- If you prefer the external terminal flow, set `CLAUDE_AUTH_DISABLE_PTY=1`.
|
|
||||||
- If you later add native modules beyond the prebuilt PTY, you may still need VS Build Tools + Python to rebuild those.
|
|
||||||
|
|
||||||
**Step 3:** Run the Claude Code setup token command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
claude setup-token
|
|
||||||
```
|
|
||||||
|
|
||||||
> **⚠️ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching.
|
|
||||||
|
|
||||||
**Step 4:** Export the Claude Code OAuth token in your shell:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 5:** Start the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev:electron
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start both the Next.js development server and the Electron application.
|
|
||||||
|
|
||||||
### Auth smoke test (Windows)
|
|
||||||
|
|
||||||
1. Ensure dependencies are installed (prebuilt pty is included).
|
|
||||||
2. Run `npm run dev:electron` and open the Setup modal.
|
|
||||||
3. Click Start on Claude auth; watch the embedded terminal stream logs.
|
|
||||||
4. Successful runs show “Token captured automatically.”; otherwise copy/paste the token from the log.
|
|
||||||
5. Optional: `node --test tests/claude-cli-detector.test.js` to verify token parsing.
|
|
||||||
|
|
||||||
**Step 6:** MOST IMPORTANT: Run the Following after all is setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
echo "W"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
|
|
||||||
- 🤖 **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress"
|
|
||||||
- 🧠 **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more
|
|
||||||
- 💭 **Extended Thinking** - Enable extended thinking modes for complex problem-solving
|
|
||||||
- 📡 **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented
|
|
||||||
- 🔍 **Project Analysis** - AI-powered project structure analysis to understand your codebase
|
|
||||||
- 📁 **Context Management** - Add context files to help AI agents understand your project better
|
|
||||||
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on your project
|
|
||||||
- 🖼️ **Image Support** - Attach images and screenshots to feature descriptions
|
|
||||||
- ⚡ **Concurrent Processing** - Configure concurrency to process multiple features simultaneously
|
|
||||||
- 🧪 **Test Integration** - Automatic test running and verification for implemented features
|
|
||||||
- 🔀 **Git Integration** - View git diffs and track changes made by AI agents
|
|
||||||
- 👤 **AI Profiles** - Create and manage different AI agent profiles for various tasks
|
|
||||||
- 💬 **Chat History** - Keep track of conversations and interactions with AI agents
|
|
||||||
- ⌨️ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts
|
|
||||||
- 🎨 **Dark/Light Theme** - Beautiful UI with theme support
|
|
||||||
- 🖥️ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org) - React framework
|
|
||||||
- [Electron](https://www.electronjs.org/) - Desktop application framework
|
|
||||||
- [Tailwind CSS](https://tailwindcss.com/) - Styling
|
|
||||||
- [Zustand](https://zustand-demo.pmnd.rs/) - State management
|
|
||||||
- [dnd-kit](https://dndkit.com/) - Drag and drop functionality
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
See [LICENSE](../LICENSE) for details.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-require-imports": "off",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simplified Electron main process
|
|
||||||
*
|
|
||||||
* This version spawns the backend server and uses HTTP API for most operations.
|
|
||||||
* Only native features (dialogs, shell) use IPC.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const { spawn } = require("child_process");
|
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
|
||||||
|
|
||||||
let mainWindow = null;
|
|
||||||
let serverProcess = null;
|
|
||||||
const SERVER_PORT = 3008;
|
|
||||||
|
|
||||||
// Get icon path - works in both dev and production
|
|
||||||
function getIconPath() {
|
|
||||||
return app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, "app", "public", "logo.png")
|
|
||||||
: path.join(__dirname, "../public/logo.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the backend server
|
|
||||||
*/
|
|
||||||
async function startServer() {
|
|
||||||
const isDev = !app.isPackaged;
|
|
||||||
|
|
||||||
// Server entry point
|
|
||||||
const serverPath = isDev
|
|
||||||
? path.join(__dirname, "../../server/dist/index.js")
|
|
||||||
: path.join(process.resourcesPath, "server", "index.js");
|
|
||||||
|
|
||||||
// Set environment variables for server
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
PORT: SERVER_PORT.toString(),
|
|
||||||
DATA_DIR: app.getPath("userData"),
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("[Electron] Starting backend server...");
|
|
||||||
|
|
||||||
serverProcess = spawn("node", [serverPath], {
|
|
||||||
env,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
serverProcess.stdout.on("data", (data) => {
|
|
||||||
console.log(`[Server] ${data.toString().trim()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverProcess.stderr.on("data", (data) => {
|
|
||||||
console.error(`[Server Error] ${data.toString().trim()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverProcess.on("close", (code) => {
|
|
||||||
console.log(`[Server] Process exited with code ${code}`);
|
|
||||||
serverProcess = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for server to be ready
|
|
||||||
await waitForServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for server to be available
|
|
||||||
*/
|
|
||||||
async function waitForServer(maxAttempts = 30) {
|
|
||||||
const http = require("http");
|
|
||||||
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
try {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => {
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Status: ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
req.on("error", reject);
|
|
||||||
req.setTimeout(1000, () => {
|
|
||||||
req.destroy();
|
|
||||||
reject(new Error("Timeout"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log("[Electron] Server is ready");
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Server failed to start");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the main window
|
|
||||||
*/
|
|
||||||
function createWindow() {
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1400,
|
|
||||||
height: 900,
|
|
||||||
minWidth: 1024,
|
|
||||||
minHeight: 700,
|
|
||||||
icon: getIconPath(),
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, "preload.js"),
|
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false,
|
|
||||||
},
|
|
||||||
titleBarStyle: "hiddenInset",
|
|
||||||
backgroundColor: "#0a0a0a",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load Next.js dev server in development or production build
|
|
||||||
const isDev = !app.isPackaged;
|
|
||||||
if (isDev) {
|
|
||||||
mainWindow.loadURL("http://localhost:3007");
|
|
||||||
if (process.env.OPEN_DEVTOOLS === "true") {
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle external links - open in default browser
|
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
||||||
shell.openExternal(url);
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// App lifecycle
|
|
||||||
app.whenReady().then(async () => {
|
|
||||||
// Set app icon (dock icon on macOS)
|
|
||||||
if (process.platform === "darwin" && app.dock) {
|
|
||||||
app.dock.setIcon(getIconPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start backend server
|
|
||||||
await startServer();
|
|
||||||
|
|
||||||
// Create window
|
|
||||||
createWindow();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Electron] Failed to start:", error);
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on("activate", () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
// Kill server process
|
|
||||||
if (serverProcess) {
|
|
||||||
console.log("[Electron] Stopping server...");
|
|
||||||
serverProcess.kill();
|
|
||||||
serverProcess = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// IPC Handlers - Only native features
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// Native file dialogs
|
|
||||||
ipcMain.handle("dialog:openDirectory", async () => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ["openDirectory", "createDirectory"],
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("dialog:openFile", async (_, options = {}) => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ["openFile"],
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("dialog:saveFile", async (_, options = {}) => {
|
|
||||||
const result = await dialog.showSaveDialog(mainWindow, options);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shell operations
|
|
||||||
ipcMain.handle("shell:openExternal", async (_, url) => {
|
|
||||||
try {
|
|
||||||
await shell.openExternal(url);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("shell:openPath", async (_, filePath) => {
|
|
||||||
try {
|
|
||||||
await shell.openPath(filePath);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// App info
|
|
||||||
ipcMain.handle("app:getPath", async (_, name) => {
|
|
||||||
return app.getPath(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("app:getVersion", async () => {
|
|
||||||
return app.getVersion();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("app:isPackaged", async () => {
|
|
||||||
return app.isPackaged;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ping - for connection check
|
|
||||||
ipcMain.handle("ping", async () => {
|
|
||||||
return "pong";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get server URL for HTTP client
|
|
||||||
ipcMain.handle("server:getUrl", async () => {
|
|
||||||
return `http://localhost:${SERVER_PORT}`;
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simplified Electron preload script
|
|
||||||
*
|
|
||||||
* Only exposes native features (dialogs, shell) and server URL.
|
|
||||||
* All other operations go through HTTP API.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require("electron");
|
|
||||||
|
|
||||||
// Expose minimal API for native features
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
|
||||||
// Platform info
|
|
||||||
platform: process.platform,
|
|
||||||
isElectron: true,
|
|
||||||
|
|
||||||
// Connection check
|
|
||||||
ping: () => ipcRenderer.invoke("ping"),
|
|
||||||
|
|
||||||
// Get server URL for HTTP client
|
|
||||||
getServerUrl: () => ipcRenderer.invoke("server:getUrl"),
|
|
||||||
|
|
||||||
// Native dialogs - better UX than prompt()
|
|
||||||
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
|
|
||||||
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
|
|
||||||
saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options),
|
|
||||||
|
|
||||||
// Shell operations
|
|
||||||
openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
|
|
||||||
openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath),
|
|
||||||
|
|
||||||
// App info
|
|
||||||
getPath: (name) => ipcRenderer.invoke("app:getPath", name),
|
|
||||||
getVersion: () => ipcRenderer.invoke("app:getVersion"),
|
|
||||||
isPackaged: () => ipcRenderer.invoke("app:isPackaged"),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[Preload] Electron API exposed (simplified mode)");
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
||||||
import nextTs from "eslint-config-next/typescript";
|
|
||||||
|
|
||||||
const eslintConfig = defineConfig([
|
|
||||||
...nextVitals,
|
|
||||||
...nextTs,
|
|
||||||
// Override default ignores of eslint-config-next.
|
|
||||||
globalIgnores([
|
|
||||||
// Default ignores of eslint-config-next:
|
|
||||||
".next/**",
|
|
||||||
"out/**",
|
|
||||||
"build/**",
|
|
||||||
"next-env.d.ts",
|
|
||||||
// Electron files use CommonJS
|
|
||||||
"electron/**",
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default eslintConfig;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
env: {
|
|
||||||
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@automaker/app",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "An autonomous AI development studio that helps you build software faster using AI-powered agents",
|
|
||||||
"homepage": "https://github.com/AutoMaker-Org/automaker",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/AutoMaker-Org/automaker.git"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "Cody Seibert",
|
|
||||||
"email": "webdevcody@gmail.com"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"license": "Unlicense",
|
|
||||||
"main": "electron/main.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev -p 3007",
|
|
||||||
"dev:web": "next dev -p 3007",
|
|
||||||
"dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"",
|
|
||||||
"dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"",
|
|
||||||
"build": "next build",
|
|
||||||
"build:electron": "next build && electron-builder",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "eslint",
|
|
||||||
"test": "playwright test",
|
|
||||||
"test:headed": "playwright test --headed",
|
|
||||||
"dev:electron:wsl": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron . --no-sandbox --disable-gpu\"",
|
|
||||||
"dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\""
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@tanstack/react-query": "^5.90.12",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"geist": "^1.5.1",
|
|
||||||
"lucide-react": "^0.556.0",
|
|
||||||
"next": "16.0.7",
|
|
||||||
"react": "19.2.0",
|
|
||||||
"react-dom": "19.2.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"sonner": "^2.0.7",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"zustand": "^5.0.9"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"lightningcss-darwin-arm64": "^1.29.2",
|
|
||||||
"lightningcss-darwin-x64": "^1.29.2",
|
|
||||||
"lightningcss-linux-arm-gnueabihf": "^1.29.2",
|
|
||||||
"lightningcss-linux-arm64-gnu": "^1.29.2",
|
|
||||||
"lightningcss-linux-arm64-musl": "^1.29.2",
|
|
||||||
"lightningcss-linux-x64-gnu": "^1.29.2",
|
|
||||||
"lightningcss-linux-x64-musl": "^1.29.2",
|
|
||||||
"lightningcss-win32-arm64-msvc": "^1.29.2",
|
|
||||||
"lightningcss-win32-x64-msvc": "^1.29.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@electron/rebuild": "^4.0.2",
|
|
||||||
"@playwright/test": "^1.57.0",
|
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"@types/node": "^20",
|
|
||||||
"@types/react": "^19",
|
|
||||||
"@types/react-dom": "^19",
|
|
||||||
"concurrently": "^9.2.1",
|
|
||||||
"electron": "^39.2.6",
|
|
||||||
"electron-builder": "^26.0.12",
|
|
||||||
"eslint": "^9",
|
|
||||||
"eslint-config-next": "16.0.7",
|
|
||||||
"tailwindcss": "^4",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"typescript": "^5",
|
|
||||||
"wait-on": "^9.0.3"
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "com.automaker.app",
|
|
||||||
"productName": "Automaker",
|
|
||||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"electron/**/*",
|
|
||||||
".next/**/*",
|
|
||||||
"public/**/*",
|
|
||||||
"!node_modules/**/*"
|
|
||||||
],
|
|
||||||
"extraResources": [
|
|
||||||
{
|
|
||||||
"from": ".env",
|
|
||||||
"to": ".env",
|
|
||||||
"filter": [
|
|
||||||
"**/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mac": {
|
|
||||||
"category": "public.app-category.developer-tools",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "dmg",
|
|
||||||
"arch": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "zip",
|
|
||||||
"arch": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"icon": "public/logo_larger.png"
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "nsis",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"icon": "public/logo_larger.png"
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "AppImage",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "deb",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"category": "Development",
|
|
||||||
"icon": "public/logo_larger.png",
|
|
||||||
"maintainer": "webdevcody@gmail.com",
|
|
||||||
"executableName": "automaker"
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"createDesktopShortcut": true,
|
|
||||||
"createStartMenuShortcut": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
|
||||||
|
|
||||||
const port = process.env.TEST_PORT || 3007;
|
|
||||||
const reuseServer = process.env.TEST_REUSE_SERVER === "true";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: "./tests",
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: "html",
|
|
||||||
timeout: 30000,
|
|
||||||
use: {
|
|
||||||
baseURL: `http://localhost:${port}`,
|
|
||||||
trace: "on-first-retry",
|
|
||||||
screenshot: "only-on-failure",
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "chromium",
|
|
||||||
use: { ...devices["Desktop Chrome"] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
...(reuseServer
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
webServer: {
|
|
||||||
command: `npx next dev -p ${port}`,
|
|
||||||
url: `http://localhost:${port}`,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
timeout: 120000,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
|
||||||
|
|
||||||
const port = process.env.TEST_PORT || 3007;
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: "./tests",
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: "html",
|
|
||||||
timeout: 10000,
|
|
||||||
use: {
|
|
||||||
baseURL: `http://localhost:${port}`,
|
|
||||||
trace: "on-first-retry",
|
|
||||||
screenshot: "only-on-failure",
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "chromium",
|
|
||||||
use: { ...devices["Desktop Chrome"] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
webServer: {
|
|
||||||
command: `npx next dev -p ${port}`,
|
|
||||||
url: `http://localhost:${port}`,
|
|
||||||
reuseExistingServer: true,
|
|
||||||
timeout: 60000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 262 KiB |
Binary file not shown.
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
query,
|
|
||||||
Options,
|
|
||||||
SDKAssistantMessage,
|
|
||||||
} from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const systemPrompt = `You are an AI assistant helping users build software. You are part of the Automaker application,
|
|
||||||
which is designed to help developers plan, design, and implement software projects autonomously.
|
|
||||||
|
|
||||||
Your role is to:
|
|
||||||
- Help users define their project requirements and specifications
|
|
||||||
- Ask clarifying questions to better understand their needs
|
|
||||||
- Suggest technical approaches and architectures
|
|
||||||
- Guide them through the development process
|
|
||||||
- Be conversational and helpful
|
|
||||||
- Write, edit, and modify code files as requested
|
|
||||||
- Execute commands and tests
|
|
||||||
- Search and analyze the codebase
|
|
||||||
|
|
||||||
When discussing projects, help users think through:
|
|
||||||
- Core functionality and features
|
|
||||||
- Technical stack choices
|
|
||||||
- Data models and architecture
|
|
||||||
- User experience considerations
|
|
||||||
- Testing strategies
|
|
||||||
|
|
||||||
You have full access to the codebase and can:
|
|
||||||
- Read files to understand existing code
|
|
||||||
- Write new files
|
|
||||||
- Edit existing files
|
|
||||||
- Run bash commands
|
|
||||||
- Search for code patterns
|
|
||||||
- Execute tests and builds`;
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { messages, workingDirectory } = await request.json();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[API] CLAUDE_CODE_OAUTH_TOKEN present:",
|
|
||||||
!!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "CLAUDE_CODE_OAUTH_TOKEN not configured" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the last user message
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
|
|
||||||
// Determine working directory - default to parent of app directory
|
|
||||||
const cwd = workingDirectory || path.resolve(process.cwd(), "..");
|
|
||||||
|
|
||||||
console.log("[API] Working directory:", cwd);
|
|
||||||
|
|
||||||
// Create query with options that enable code modification
|
|
||||||
const options: Options = {
|
|
||||||
// model: "claude-sonnet-4-20250514",
|
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
systemPrompt,
|
|
||||||
maxTurns: 20,
|
|
||||||
cwd,
|
|
||||||
// Enable all core tools for code modification
|
|
||||||
allowedTools: [
|
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
// Auto-accept file edits within the working directory
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
// Enable sandbox for safer bash execution
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert message history to SDK format to preserve conversation context
|
|
||||||
// Include both user and assistant messages for full context
|
|
||||||
const sessionId = `api-session-${Date.now()}`;
|
|
||||||
const conversationMessages = messages.map(
|
|
||||||
(msg: { role: string; content: string }) => {
|
|
||||||
if (msg.role === "user") {
|
|
||||||
return {
|
|
||||||
type: "user" as const,
|
|
||||||
message: {
|
|
||||||
role: "user" as const,
|
|
||||||
content: msg.content,
|
|
||||||
},
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
session_id: sessionId,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Assistant message
|
|
||||||
return {
|
|
||||||
type: "assistant" as const,
|
|
||||||
message: {
|
|
||||||
role: "assistant" as const,
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: msg.content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
session_id: sessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute query with full conversation context
|
|
||||||
const queryResult = query({
|
|
||||||
prompt:
|
|
||||||
conversationMessages.length > 0
|
|
||||||
? conversationMessages
|
|
||||||
: lastMessage.content,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
let responseText = "";
|
|
||||||
const toolUses: Array<{ name: string; input: unknown }> = [];
|
|
||||||
|
|
||||||
// Collect the response from the async generator
|
|
||||||
for await (const msg of queryResult) {
|
|
||||||
if (msg.type === "assistant") {
|
|
||||||
const assistantMsg = msg as SDKAssistantMessage;
|
|
||||||
if (assistantMsg.message.content) {
|
|
||||||
for (const block of assistantMsg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
responseText += block.text;
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
// Track tool usage for transparency
|
|
||||||
toolUses.push({
|
|
||||||
name: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === "result") {
|
|
||||||
if (msg.subtype === "success") {
|
|
||||||
if (msg.result) {
|
|
||||||
responseText = msg.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
content: responseText || "Sorry, I couldn't generate a response.",
|
|
||||||
toolUses: toolUses.length > 0 ? toolUses : undefined,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Claude API error:", error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Failed to get response from Claude";
|
|
||||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
|
|
||||||
interface AnthropicResponse {
|
|
||||||
content?: Array<{ type: string; text?: string }>;
|
|
||||||
model?: string;
|
|
||||||
error?: { message?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { apiKey } = await request.json();
|
|
||||||
|
|
||||||
// Use provided API key or fall back to environment variable
|
|
||||||
const effectiveApiKey = apiKey || process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
||||||
|
|
||||||
if (!effectiveApiKey) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "No API key provided or configured in environment" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a simple test prompt to the Anthropic API
|
|
||||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": effectiveApiKey,
|
|
||||||
"anthropic-version": "2023-06-01",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: "claude-sonnet-4-20250514",
|
|
||||||
max_tokens: 100,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: "Respond with exactly: 'Claude API connection successful!' and nothing else.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = (await response.json()) as AnthropicResponse;
|
|
||||||
const errorMessage = errorData.error?.message || `HTTP ${response.status}`;
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "Invalid API key. Please check your Anthropic API key." },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 429) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "Rate limit exceeded. Please try again later." },
|
|
||||||
{ status: 429 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `API error: ${errorMessage}` },
|
|
||||||
{ status: response.status }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as AnthropicResponse;
|
|
||||||
|
|
||||||
// Check if we got a valid response
|
|
||||||
if (data.content && data.content.length > 0) {
|
|
||||||
const textContent = data.content.find((block) => block.type === "text");
|
|
||||||
if (textContent && textContent.type === "text" && textContent.text) {
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Connection successful! Response: "${textContent.text}"`,
|
|
||||||
model: data.model,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: "Connection successful! Claude responded.",
|
|
||||||
model: data.model,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Claude API test error:", error);
|
|
||||||
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : "Failed to connect to Claude API";
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: errorMessage },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
|
|
||||||
interface GeminiContent {
|
|
||||||
parts: Array<{
|
|
||||||
text?: string;
|
|
||||||
inlineData?: {
|
|
||||||
mimeType: string;
|
|
||||||
data: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
role?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeminiRequest {
|
|
||||||
contents: GeminiContent[];
|
|
||||||
generationConfig?: {
|
|
||||||
maxOutputTokens?: number;
|
|
||||||
temperature?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeminiResponse {
|
|
||||||
candidates?: Array<{
|
|
||||||
content: {
|
|
||||||
parts: Array<{
|
|
||||||
text: string;
|
|
||||||
}>;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
finishReason: string;
|
|
||||||
safetyRatings?: Array<{
|
|
||||||
category: string;
|
|
||||||
probability: string;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
promptFeedback?: {
|
|
||||||
safetyRatings?: Array<{
|
|
||||||
category: string;
|
|
||||||
probability: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
error?: {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { apiKey, imageData, mimeType, prompt } = await request.json();
|
|
||||||
|
|
||||||
// Use provided API key or fall back to environment variable
|
|
||||||
const effectiveApiKey = apiKey || process.env.GOOGLE_API_KEY;
|
|
||||||
|
|
||||||
if (!effectiveApiKey) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "No API key provided or configured in environment" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the request body
|
|
||||||
const requestBody: GeminiRequest = {
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
parts: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
generationConfig: {
|
|
||||||
maxOutputTokens: 150,
|
|
||||||
temperature: 0.4,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add image if provided
|
|
||||||
if (imageData && mimeType) {
|
|
||||||
requestBody.contents[0].parts.push({
|
|
||||||
inlineData: {
|
|
||||||
mimeType: mimeType,
|
|
||||||
data: imageData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add text prompt
|
|
||||||
const textPrompt = prompt || (imageData
|
|
||||||
? "Describe what you see in this image briefly."
|
|
||||||
: "Respond with exactly: 'Gemini SDK connection successful!' and nothing else.");
|
|
||||||
|
|
||||||
requestBody.contents[0].parts.push({
|
|
||||||
text: textPrompt,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call Gemini API - using gemini-1.5-flash as it supports both text and vision
|
|
||||||
const model = imageData ? "gemini-1.5-flash" : "gemini-1.5-flash";
|
|
||||||
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${effectiveApiKey}`;
|
|
||||||
|
|
||||||
const response = await fetch(geminiUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data: GeminiResponse = await response.json();
|
|
||||||
|
|
||||||
// Check for API errors
|
|
||||||
if (data.error) {
|
|
||||||
const errorMessage = data.error.message || "Unknown Gemini API error";
|
|
||||||
const statusCode = data.error.code || 500;
|
|
||||||
|
|
||||||
if (statusCode === 400 && errorMessage.includes("API key")) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "Invalid API key. Please check your Google API key." },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode === 429) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "Rate limit exceeded. Please try again later." },
|
|
||||||
{ status: 429 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `API error: ${errorMessage}` },
|
|
||||||
{ status: statusCode }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for valid response
|
|
||||||
if (!response.ok) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `HTTP error: ${response.status} ${response.statusText}` },
|
|
||||||
{ status: response.status }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract response text
|
|
||||||
if (data.candidates && data.candidates.length > 0 && data.candidates[0].content?.parts?.length > 0) {
|
|
||||||
const responseText = data.candidates[0].content.parts
|
|
||||||
.filter((part) => part.text)
|
|
||||||
.map((part) => part.text)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Connection successful! Response: "${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}"`,
|
|
||||||
model: model,
|
|
||||||
hasImage: !!imageData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle blocked responses
|
|
||||||
if (data.promptFeedback?.safetyRatings) {
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: "Connection successful! Gemini responded (response may have been filtered).",
|
|
||||||
model: model,
|
|
||||||
hasImage: !!imageData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: "Connection successful! Gemini responded.",
|
|
||||||
model: model,
|
|
||||||
hasImage: !!imageData,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Gemini API test error:", error);
|
|
||||||
|
|
||||||
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: "Network error. Unable to reach Gemini API." },
|
|
||||||
{ status: 503 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : "Failed to connect to Gemini API";
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: errorMessage },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { GeistSans } from "geist/font/sans";
|
|
||||||
import { GeistMono } from "geist/font/mono";
|
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import "./globals.css";
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Automaker - Autonomous AI Development Studio",
|
|
||||||
description: "Build software autonomously with intelligent orchestration",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<html lang="en" suppressHydrationWarning>
|
|
||||||
<body
|
|
||||||
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<Toaster richColors position="bottom-right" />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { WelcomeView } from "@/components/views/welcome-view";
|
|
||||||
import { BoardView } from "@/components/views/board-view";
|
|
||||||
import { SpecView } from "@/components/views/spec-view";
|
|
||||||
import { AgentView } from "@/components/views/agent-view";
|
|
||||||
import { SettingsView } from "@/components/views/settings-view";
|
|
||||||
import { InterviewView } from "@/components/views/interview-view";
|
|
||||||
import { ContextView } from "@/components/views/context-view";
|
|
||||||
import { ProfilesView } from "@/components/views/profiles-view";
|
|
||||||
import { SetupView } from "@/components/views/setup-view";
|
|
||||||
import { RunningAgentsView } from "@/components/views/running-agents-view";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import { useSetupStore } from "@/store/setup-store";
|
|
||||||
import { getElectronAPI, isElectron } from "@/lib/electron";
|
|
||||||
import {
|
|
||||||
FileBrowserProvider,
|
|
||||||
useFileBrowser,
|
|
||||||
setGlobalFileBrowser,
|
|
||||||
} from "@/contexts/file-browser-context";
|
|
||||||
|
|
||||||
function HomeContent() {
|
|
||||||
const {
|
|
||||||
currentView,
|
|
||||||
setCurrentView,
|
|
||||||
setIpcConnected,
|
|
||||||
theme,
|
|
||||||
currentProject,
|
|
||||||
previewTheme,
|
|
||||||
getEffectiveTheme,
|
|
||||||
} = useAppStore();
|
|
||||||
const { isFirstRun, setupComplete } = useSetupStore();
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
|
||||||
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
|
||||||
const { openFileBrowser } = useFileBrowser();
|
|
||||||
|
|
||||||
// Hidden streamer panel - opens with "\" key
|
|
||||||
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
|
|
||||||
// Don't trigger when typing in inputs
|
|
||||||
const activeElement = document.activeElement;
|
|
||||||
if (activeElement) {
|
|
||||||
const tagName = activeElement.tagName.toLowerCase();
|
|
||||||
if (
|
|
||||||
tagName === "input" ||
|
|
||||||
tagName === "textarea" ||
|
|
||||||
tagName === "select"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeElement.getAttribute("contenteditable") === "true") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const role = activeElement.getAttribute("role");
|
|
||||||
if (role === "textbox" || role === "searchbox" || role === "combobox") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't trigger with modifier keys
|
|
||||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for "\" key (backslash)
|
|
||||||
if (event.key === "\\") {
|
|
||||||
event.preventDefault();
|
|
||||||
setStreamerPanelOpen((prev) => !prev);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Register the "\" shortcut for streamer panel
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("keydown", handleStreamerPanelShortcut);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", handleStreamerPanelShortcut);
|
|
||||||
};
|
|
||||||
}, [handleStreamerPanelShortcut]);
|
|
||||||
|
|
||||||
// Compute the effective theme: previewTheme takes priority, then project theme, then global theme
|
|
||||||
// This is reactive because it depends on previewTheme, currentProject, and theme from the store
|
|
||||||
const effectiveTheme = getEffectiveTheme();
|
|
||||||
|
|
||||||
// Prevent hydration issues
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Initialize global file browser for HttpApiClient
|
|
||||||
useEffect(() => {
|
|
||||||
setGlobalFileBrowser(openFileBrowser);
|
|
||||||
}, [openFileBrowser]);
|
|
||||||
|
|
||||||
// Check if this is first run and redirect to setup if needed
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[Setup Flow] Checking setup state:", {
|
|
||||||
isMounted,
|
|
||||||
isFirstRun,
|
|
||||||
setupComplete,
|
|
||||||
currentView,
|
|
||||||
shouldShowSetup: isMounted && isFirstRun && !setupComplete,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isMounted && isFirstRun && !setupComplete) {
|
|
||||||
console.log(
|
|
||||||
"[Setup Flow] Redirecting to setup wizard (first run, not complete)"
|
|
||||||
);
|
|
||||||
setCurrentView("setup");
|
|
||||||
} else if (isMounted && setupComplete) {
|
|
||||||
console.log("[Setup Flow] Setup already complete, showing normal view");
|
|
||||||
}
|
|
||||||
}, [isMounted, isFirstRun, setupComplete, setCurrentView, currentView]);
|
|
||||||
|
|
||||||
// Test IPC connection on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const testConnection = async () => {
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
const result = await api.ping();
|
|
||||||
setIpcConnected(result === "pong");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("IPC connection failed:", error);
|
|
||||||
setIpcConnected(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
testConnection();
|
|
||||||
}, [setIpcConnected]);
|
|
||||||
|
|
||||||
// Apply theme class to document (uses effective theme - preview, project-specific, or global)
|
|
||||||
useEffect(() => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.classList.remove(
|
|
||||||
"dark",
|
|
||||||
"retro",
|
|
||||||
"light",
|
|
||||||
"dracula",
|
|
||||||
"nord",
|
|
||||||
"monokai",
|
|
||||||
"tokyonight",
|
|
||||||
"solarized",
|
|
||||||
"gruvbox",
|
|
||||||
"catppuccin",
|
|
||||||
"onedark",
|
|
||||||
"synthwave",
|
|
||||||
"red"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (effectiveTheme === "dark") {
|
|
||||||
root.classList.add("dark");
|
|
||||||
} else if (effectiveTheme === "retro") {
|
|
||||||
root.classList.add("retro");
|
|
||||||
} else if (effectiveTheme === "dracula") {
|
|
||||||
root.classList.add("dracula");
|
|
||||||
} else if (effectiveTheme === "nord") {
|
|
||||||
root.classList.add("nord");
|
|
||||||
} else if (effectiveTheme === "monokai") {
|
|
||||||
root.classList.add("monokai");
|
|
||||||
} else if (effectiveTheme === "tokyonight") {
|
|
||||||
root.classList.add("tokyonight");
|
|
||||||
} else if (effectiveTheme === "solarized") {
|
|
||||||
root.classList.add("solarized");
|
|
||||||
} else if (effectiveTheme === "gruvbox") {
|
|
||||||
root.classList.add("gruvbox");
|
|
||||||
} else if (effectiveTheme === "catppuccin") {
|
|
||||||
root.classList.add("catppuccin");
|
|
||||||
} else if (effectiveTheme === "onedark") {
|
|
||||||
root.classList.add("onedark");
|
|
||||||
} else if (effectiveTheme === "synthwave") {
|
|
||||||
root.classList.add("synthwave");
|
|
||||||
} else if (effectiveTheme === "red") {
|
|
||||||
root.classList.add("red");
|
|
||||||
} else if (effectiveTheme === "light") {
|
|
||||||
root.classList.add("light");
|
|
||||||
} else if (effectiveTheme === "system") {
|
|
||||||
// System theme
|
|
||||||
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
||||||
if (isDark) {
|
|
||||||
root.classList.add("dark");
|
|
||||||
} else {
|
|
||||||
root.classList.add("light");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [effectiveTheme, previewTheme, currentProject, theme]);
|
|
||||||
|
|
||||||
const renderView = () => {
|
|
||||||
switch (currentView) {
|
|
||||||
case "welcome":
|
|
||||||
return <WelcomeView />;
|
|
||||||
case "setup":
|
|
||||||
return <SetupView />;
|
|
||||||
case "board":
|
|
||||||
return <BoardView />;
|
|
||||||
case "spec":
|
|
||||||
return <SpecView />;
|
|
||||||
case "agent":
|
|
||||||
return <AgentView />;
|
|
||||||
case "settings":
|
|
||||||
return <SettingsView />;
|
|
||||||
case "interview":
|
|
||||||
return <InterviewView />;
|
|
||||||
case "context":
|
|
||||||
return <ContextView />;
|
|
||||||
case "profiles":
|
|
||||||
return <ProfilesView />;
|
|
||||||
case "running-agents":
|
|
||||||
return <RunningAgentsView />;
|
|
||||||
default:
|
|
||||||
return <WelcomeView />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup view is full-screen without sidebar
|
|
||||||
if (currentView === "setup") {
|
|
||||||
return (
|
|
||||||
<main className="h-screen overflow-hidden" data-testid="app-container">
|
|
||||||
<SetupView />
|
|
||||||
{/* Environment indicator */}
|
|
||||||
{isMounted && !isElectron() && (
|
|
||||||
<div className="fixed bottom-4 right-4 px-3 py-1.5 bg-blue-500/10 text-blue-500 text-xs rounded-full border border-blue-500/20 pointer-events-none">
|
|
||||||
Web Mode
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex h-screen overflow-hidden" data-testid="app-container">
|
|
||||||
<Sidebar />
|
|
||||||
<div
|
|
||||||
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
|
||||||
style={{ marginRight: streamerPanelOpen ? "250px" : "0" }}
|
|
||||||
>
|
|
||||||
{renderView()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Environment indicator - only show after mount to prevent hydration issues */}
|
|
||||||
{isMounted && !isElectron() && (
|
|
||||||
<div className="fixed bottom-4 right-4 px-3 py-1.5 bg-blue-500/10 text-blue-500 text-xs rounded-full border border-blue-500/20 pointer-events-none">
|
|
||||||
Web Mode
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
|
||||||
<div
|
|
||||||
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
|
||||||
streamerPanelOpen ? "translate-x-0" : "translate-x-full"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<FileBrowserProvider>
|
|
||||||
<HomeContent />
|
|
||||||
</FileBrowserProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { FolderOpen, Folder, ChevronRight, Home, ArrowLeft, HardDrive } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
interface DirectoryEntry {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BrowseResult {
|
|
||||||
success: boolean;
|
|
||||||
currentPath: string;
|
|
||||||
parentPath: string | null;
|
|
||||||
directories: DirectoryEntry[];
|
|
||||||
drives?: string[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileBrowserDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
onSelect: (path: string) => void;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FileBrowserDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
onSelect,
|
|
||||||
title = "Select Project Directory",
|
|
||||||
description = "Navigate to your project folder",
|
|
||||||
}: FileBrowserDialogProps) {
|
|
||||||
const [currentPath, setCurrentPath] = useState<string>("");
|
|
||||||
const [parentPath, setParentPath] = useState<string | null>(null);
|
|
||||||
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
|
|
||||||
const [drives, setDrives] = useState<string[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const browseDirectory = async (dirPath?: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get server URL from environment or default
|
|
||||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
|
|
||||||
|
|
||||||
const response = await fetch(`${serverUrl}/api/fs/browse`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ dirPath }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: BrowseResult = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setCurrentPath(result.currentPath);
|
|
||||||
setParentPath(result.parentPath);
|
|
||||||
setDirectories(result.directories);
|
|
||||||
setDrives(result.drives || []);
|
|
||||||
} else {
|
|
||||||
setError(result.error || "Failed to browse directory");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Failed to load directories");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load home directory on mount
|
|
||||||
useEffect(() => {
|
|
||||||
if (open && !currentPath) {
|
|
||||||
browseDirectory();
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const handleSelectDirectory = (dir: DirectoryEntry) => {
|
|
||||||
browseDirectory(dir.path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoToParent = () => {
|
|
||||||
if (parentPath) {
|
|
||||||
browseDirectory(parentPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoHome = () => {
|
|
||||||
browseDirectory();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectDrive = (drivePath: string) => {
|
|
||||||
browseDirectory(drivePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = () => {
|
|
||||||
if (currentPath) {
|
|
||||||
onSelect(currentPath);
|
|
||||||
onOpenChange(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
<FolderOpen className="w-5 h-5 text-brand-500" />
|
|
||||||
{title}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="text-muted-foreground">
|
|
||||||
{description}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 min-h-[400px]">
|
|
||||||
{/* Drives selector (Windows only) */}
|
|
||||||
{drives.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mr-2">
|
|
||||||
<HardDrive className="w-3 h-3" />
|
|
||||||
<span>Drives:</span>
|
|
||||||
</div>
|
|
||||||
{drives.map((drive) => (
|
|
||||||
<Button
|
|
||||||
key={drive}
|
|
||||||
variant={currentPath.startsWith(drive) ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleSelectDrive(drive)}
|
|
||||||
className="h-7 px-3 text-xs"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{drive.replace("\\", "")}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current path breadcrumb */}
|
|
||||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleGoHome}
|
|
||||||
className="h-7 px-2"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Home className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
{parentPath && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleGoToParent}
|
|
||||||
className="h-7 px-2"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 font-mono text-sm truncate text-muted-foreground">
|
|
||||||
{currentPath || "Loading..."}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Directory list */}
|
|
||||||
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
|
|
||||||
{loading && (
|
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
|
||||||
<div className="text-sm text-muted-foreground">Loading directories...</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
|
||||||
<div className="text-sm text-destructive">{error}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && !error && directories.length === 0 && (
|
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
|
||||||
<div className="text-sm text-muted-foreground">No subdirectories found</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && !error && directories.length > 0 && (
|
|
||||||
<div className="divide-y divide-sidebar-border">
|
|
||||||
{directories.map((dir) => (
|
|
||||||
<button
|
|
||||||
key={dir.path}
|
|
||||||
onClick={() => handleSelectDirectory(dir)}
|
|
||||||
className="w-full flex items-center gap-3 p-3 hover:bg-sidebar-accent/10 transition-colors text-left group"
|
|
||||||
>
|
|
||||||
<Folder className="w-5 h-5 text-brand-500 shrink-0" />
|
|
||||||
<span className="flex-1 truncate text-sm">{dir.name}</span>
|
|
||||||
<ChevronRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
Click on a folder to navigate. Select the current folder or navigate to a subfolder.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
|
||||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSelect} disabled={!currentPath || loading}>
|
|
||||||
<FolderOpen className="w-4 h-4 mr-2" />
|
|
||||||
Select Current Folder
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface BadgeProps
|
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants };
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
"animated-outline":
|
|
||||||
"relative overflow-hidden rounded-xl hover:bg-transparent shadow-none",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
"icon-sm": "size-8",
|
|
||||||
"icon-lg": "size-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
}) {
|
|
||||||
// Special handling for animated-outline variant
|
|
||||||
if (variant === "animated-outline" && !asChild) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({ variant, size }),
|
|
||||||
"p-[1px]", // Force 1px padding for the gradient border
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
data-slot="button"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{/* Animated rotating gradient border */}
|
|
||||||
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] animated-outline-gradient" />
|
|
||||||
|
|
||||||
{/* Inner content container */}
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"animated-outline-inner inline-flex h-full w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 py-1 text-sm font-medium backdrop-blur-3xl transition-all",
|
|
||||||
size === "sm" && "px-3 text-xs gap-1.5",
|
|
||||||
size === "lg" && "px-8",
|
|
||||||
size === "icon" && "p-0 gap-0"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Comp = asChild ? Slot : "button";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Comp>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
className={cn(
|
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-white/10 backdrop-blur-sm py-6 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn("leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-action"
|
|
||||||
className={cn(
|
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-content"
|
|
||||||
className={cn("px-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { Check, ChevronsUpDown } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
|
|
||||||
interface CategoryAutocompleteProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
suggestions: string[];
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
"data-testid"?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CategoryAutocomplete({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
suggestions,
|
|
||||||
placeholder = "Select or type a category...",
|
|
||||||
className,
|
|
||||||
disabled = false,
|
|
||||||
"data-testid": testId,
|
|
||||||
}: CategoryAutocompleteProps) {
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={open}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn("w-full justify-between", className)}
|
|
||||||
data-testid={testId}
|
|
||||||
>
|
|
||||||
{value
|
|
||||||
? suggestions.find((s) => s === value) ?? value
|
|
||||||
: placeholder}
|
|
||||||
<ChevronsUpDown className="opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[200px] p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search category..." className="h-9" />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No category found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{suggestions.map((suggestion) => (
|
|
||||||
<CommandItem
|
|
||||||
key={suggestion}
|
|
||||||
value={suggestion}
|
|
||||||
onSelect={(currentValue) => {
|
|
||||||
onChange(currentValue === value ? "" : currentValue);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
data-testid={`category-option-${suggestion.toLowerCase().replace(/\s+/g, "-")}`}
|
|
||||||
>
|
|
||||||
{suggestion}
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"ml-auto",
|
|
||||||
value === suggestion ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
className={cn("flex items-center justify-center text-current")}
|
|
||||||
>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
));
|
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Checkbox };
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { Sparkles, X } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
interface CoursePromoBadgeProps {
|
|
||||||
sidebarOpen?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CoursePromoBadge({ sidebarOpen = true }: CoursePromoBadgeProps) {
|
|
||||||
const [dismissed, setDismissed] = React.useState(false);
|
|
||||||
|
|
||||||
if (dismissed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapsed state - show only icon with tooltip
|
|
||||||
if (!sidebarOpen) {
|
|
||||||
return (
|
|
||||||
<div className="p-2 pb-0 flex justify-center">
|
|
||||||
<TooltipProvider delayDuration={300}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<a
|
|
||||||
href="https://agenticjumpstart.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group cursor-pointer flex items-center justify-center w-10 h-10 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-all border border-primary/30"
|
|
||||||
data-testid="course-promo-badge-collapsed"
|
|
||||||
>
|
|
||||||
<Sparkles className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" className="flex items-center gap-2">
|
|
||||||
<span>Become a 10x Dev</span>
|
|
||||||
<span
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDismissed(true);
|
|
||||||
}}
|
|
||||||
className="p-0.5 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
|
|
||||||
aria-label="Dismiss"
|
|
||||||
>
|
|
||||||
<X className="size-3" />
|
|
||||||
</span>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expanded state - show full badge
|
|
||||||
return (
|
|
||||||
<div className="p-2 pb-0">
|
|
||||||
<a
|
|
||||||
href="https://agenticjumpstart.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group cursor-pointer flex items-center justify-between w-full px-2 lg:px-3 py-2.5 bg-primary/10 text-primary rounded-lg font-medium text-sm hover:bg-primary/20 transition-all border border-primary/30"
|
|
||||||
data-testid="course-promo-badge"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Sparkles className="size-4 shrink-0" />
|
|
||||||
<span className="hidden lg:block">Become a 10x Dev</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDismissed(true);
|
|
||||||
}}
|
|
||||||
className="hidden lg:block p-1 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
|
|
||||||
aria-label="Dismiss"
|
|
||||||
>
|
|
||||||
<X className="size-3.5" />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useRef, useCallback } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ImageIcon, X, Loader2 } from "lucide-react";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
|
|
||||||
export interface FeatureImagePath {
|
|
||||||
id: string;
|
|
||||||
path: string; // Path to the temp file
|
|
||||||
filename: string;
|
|
||||||
mimeType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map to store preview data by image ID (persisted across component re-mounts)
|
|
||||||
export type ImagePreviewMap = Map<string, string>;
|
|
||||||
|
|
||||||
interface DescriptionImageDropZoneProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
images: FeatureImagePath[];
|
|
||||||
onImagesChange: (images: FeatureImagePath[]) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
maxFiles?: number;
|
|
||||||
maxFileSize?: number; // in bytes, default 10MB
|
|
||||||
// Optional: pass preview map from parent to persist across tab switches
|
|
||||||
previewMap?: ImagePreviewMap;
|
|
||||||
onPreviewMapChange?: (map: ImagePreviewMap) => void;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
error?: boolean; // Show error state with red border
|
|
||||||
}
|
|
||||||
|
|
||||||
const ACCEPTED_IMAGE_TYPES = [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/jpg",
|
|
||||||
"image/png",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp",
|
|
||||||
];
|
|
||||||
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
||||||
|
|
||||||
export function DescriptionImageDropZone({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
images,
|
|
||||||
onImagesChange,
|
|
||||||
placeholder = "Describe the feature...",
|
|
||||||
className,
|
|
||||||
disabled = false,
|
|
||||||
maxFiles = 5,
|
|
||||||
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
||||||
previewMap,
|
|
||||||
onPreviewMapChange,
|
|
||||||
autoFocus = false,
|
|
||||||
error = false,
|
|
||||||
}: DescriptionImageDropZoneProps) {
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
// Use parent-provided preview map if available, otherwise use local state
|
|
||||||
const [localPreviewImages, setLocalPreviewImages] = useState<Map<string, string>>(
|
|
||||||
() => new Map()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine which preview map to use - prefer parent-controlled state
|
|
||||||
const previewImages = previewMap !== undefined ? previewMap : localPreviewImages;
|
|
||||||
const setPreviewImages = useCallback((updater: Map<string, string> | ((prev: Map<string, string>) => Map<string, string>)) => {
|
|
||||||
if (onPreviewMapChange) {
|
|
||||||
const currentMap = previewMap !== undefined ? previewMap : localPreviewImages;
|
|
||||||
const newMap = typeof updater === 'function' ? updater(currentMap) : updater;
|
|
||||||
onPreviewMapChange(newMap);
|
|
||||||
} else {
|
|
||||||
setLocalPreviewImages((prev) => {
|
|
||||||
const newMap = typeof updater === 'function' ? updater(prev) : updater;
|
|
||||||
return newMap;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [onPreviewMapChange, previewMap, localPreviewImages]);
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const currentProject = useAppStore((state) => state.currentProject);
|
|
||||||
|
|
||||||
// Construct server URL for loading saved images
|
|
||||||
const getImageServerUrl = useCallback((imagePath: string): string => {
|
|
||||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
|
|
||||||
const projectPath = currentProject?.path || "";
|
|
||||||
return `${serverUrl}/api/fs/image?path=${encodeURIComponent(imagePath)}&projectPath=${encodeURIComponent(projectPath)}`;
|
|
||||||
}, [currentProject?.path]);
|
|
||||||
|
|
||||||
const fileToBase64 = (file: File): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
if (typeof reader.result === "string") {
|
|
||||||
resolve(reader.result);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to read file as base64"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveImageToTemp = useCallback(async (
|
|
||||||
base64Data: string,
|
|
||||||
filename: string,
|
|
||||||
mimeType: string
|
|
||||||
): Promise<string | null> => {
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
// Check if saveImageToTemp method exists
|
|
||||||
if (!api.saveImageToTemp) {
|
|
||||||
// Fallback path when saveImageToTemp is not available
|
|
||||||
console.log("[DescriptionImageDropZone] Using fallback path for image");
|
|
||||||
return `.automaker/images/${Date.now()}_${filename}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get projectPath from the store if available
|
|
||||||
const projectPath = currentProject?.path;
|
|
||||||
const result = await api.saveImageToTemp(base64Data, filename, mimeType, projectPath);
|
|
||||||
if (result.success && result.path) {
|
|
||||||
return result.path;
|
|
||||||
}
|
|
||||||
console.error("[DescriptionImageDropZone] Failed to save image:", result.error);
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[DescriptionImageDropZone] Error saving image:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [currentProject?.path]);
|
|
||||||
|
|
||||||
const processFiles = useCallback(
|
|
||||||
async (files: FileList) => {
|
|
||||||
if (disabled || isProcessing) return;
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
|
||||||
const newImages: FeatureImagePath[] = [];
|
|
||||||
const newPreviews = new Map(previewImages);
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
for (const file of Array.from(files)) {
|
|
||||||
// Validate file type
|
|
||||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
|
||||||
errors.push(
|
|
||||||
`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file size
|
|
||||||
if (file.size > maxFileSize) {
|
|
||||||
const maxSizeMB = maxFileSize / (1024 * 1024);
|
|
||||||
errors.push(
|
|
||||||
`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've reached max files
|
|
||||||
if (newImages.length + images.length >= maxFiles) {
|
|
||||||
errors.push(`Maximum ${maxFiles} images allowed.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const base64 = await fileToBase64(file);
|
|
||||||
const tempPath = await saveImageToTemp(base64, file.name, file.type);
|
|
||||||
|
|
||||||
if (tempPath) {
|
|
||||||
const imageId = `img-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
||||||
const imagePathRef: FeatureImagePath = {
|
|
||||||
id: imageId,
|
|
||||||
path: tempPath,
|
|
||||||
filename: file.name,
|
|
||||||
mimeType: file.type,
|
|
||||||
};
|
|
||||||
newImages.push(imagePathRef);
|
|
||||||
// Store preview for display
|
|
||||||
newPreviews.set(imageId, base64);
|
|
||||||
} else {
|
|
||||||
errors.push(`${file.name}: Failed to save image.`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(`${file.name}: Failed to process image.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.warn("Image upload errors:", errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newImages.length > 0) {
|
|
||||||
onImagesChange([...images, ...newImages]);
|
|
||||||
setPreviewImages(newPreviews);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProcessing(false);
|
|
||||||
},
|
|
||||||
[disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange, previewImages, saveImageToTemp]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
(e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
if (disabled) return;
|
|
||||||
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
if (files.length > 0) {
|
|
||||||
processFiles(files);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disabled, processFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragOver = useCallback(
|
|
||||||
(e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!disabled) {
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disabled]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFileSelect = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
processFiles(files);
|
|
||||||
}
|
|
||||||
// Reset the input so the same file can be selected again
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[processFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleBrowseClick = useCallback(() => {
|
|
||||||
if (!disabled && fileInputRef.current) {
|
|
||||||
fileInputRef.current.click();
|
|
||||||
}
|
|
||||||
}, [disabled]);
|
|
||||||
|
|
||||||
const removeImage = useCallback(
|
|
||||||
(imageId: string) => {
|
|
||||||
onImagesChange(images.filter((img) => img.id !== imageId));
|
|
||||||
setPreviewImages((prev) => {
|
|
||||||
const newMap = new Map(prev);
|
|
||||||
newMap.delete(imageId);
|
|
||||||
return newMap;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[images, onImagesChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("relative", className)}>
|
|
||||||
{/* Hidden file input */}
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
accept={ACCEPTED_IMAGE_TYPES.join(",")}
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
className="hidden"
|
|
||||||
disabled={disabled}
|
|
||||||
data-testid="description-image-input"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Drop zone wrapper */}
|
|
||||||
<div
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
className={cn(
|
|
||||||
"relative rounded-md transition-all duration-200",
|
|
||||||
{
|
|
||||||
"ring-2 ring-blue-400 ring-offset-2 ring-offset-background":
|
|
||||||
isDragOver && !disabled,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Drag overlay */}
|
|
||||||
{isDragOver && !disabled && (
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-blue-500/20 border-2 border-dashed border-blue-400 pointer-events-none"
|
|
||||||
data-testid="drop-overlay"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center gap-2 text-blue-400">
|
|
||||||
<ImageIcon className="w-8 h-8" />
|
|
||||||
<span className="text-sm font-medium">Drop images here</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Textarea */}
|
|
||||||
<Textarea
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
disabled={disabled}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
aria-invalid={error}
|
|
||||||
className={cn(
|
|
||||||
"min-h-[120px]",
|
|
||||||
isProcessing && "opacity-50 pointer-events-none"
|
|
||||||
)}
|
|
||||||
data-testid="feature-description-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hint text */}
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Drag and drop images here or{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBrowseClick}
|
|
||||||
className="text-primary hover:text-primary/80 underline"
|
|
||||||
disabled={disabled || isProcessing}
|
|
||||||
>
|
|
||||||
browse
|
|
||||||
</button>{" "}
|
|
||||||
to attach context images
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Processing indicator */}
|
|
||||||
{isProcessing && (
|
|
||||||
<div className="flex items-center gap-2 mt-2 text-sm text-muted-foreground">
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
<span>Saving images...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Image previews */}
|
|
||||||
{images.length > 0 && (
|
|
||||||
<div className="mt-3 space-y-2" data-testid="description-image-previews">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-xs font-medium text-foreground">
|
|
||||||
{images.length} image{images.length > 1 ? "s" : ""} attached
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
onImagesChange([]);
|
|
||||||
setPreviewImages(new Map());
|
|
||||||
}}
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
Clear all
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{images.map((image) => (
|
|
||||||
<div
|
|
||||||
key={image.id}
|
|
||||||
className="relative group rounded-md border border-muted bg-muted/50 overflow-hidden"
|
|
||||||
data-testid={`description-image-preview-${image.id}`}
|
|
||||||
>
|
|
||||||
{/* Image thumbnail or placeholder */}
|
|
||||||
<div className="w-16 h-16 flex items-center justify-center bg-zinc-800">
|
|
||||||
{previewImages.has(image.id) ? (
|
|
||||||
<img
|
|
||||||
src={previewImages.get(image.id)}
|
|
||||||
alt={image.filename}
|
|
||||||
className="max-w-full max-h-full object-contain"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src={getImageServerUrl(image.path)}
|
|
||||||
alt={image.filename}
|
|
||||||
className="max-w-full max-h-full object-contain"
|
|
||||||
onError={(e) => {
|
|
||||||
// If image fails to load, hide it
|
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Remove button */}
|
|
||||||
{!disabled && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeImage(image.id);
|
|
||||||
}}
|
|
||||||
className="absolute top-0.5 right-0.5 p-0.5 rounded-full bg-destructive text-destructive-foreground opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
data-testid={`remove-description-image-${image.id}`}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{/* Filename tooltip on hover */}
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<p className="text-[10px] text-white truncate">
|
|
||||||
{image.filename}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
||||||
import { XIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Dialog({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
data-slot="dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showCloseButton = true,
|
|
||||||
compact = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
||||||
showCloseButton?: boolean;
|
|
||||||
compact?: boolean;
|
|
||||||
}) {
|
|
||||||
// Check if className contains a custom max-width
|
|
||||||
const hasCustomMaxWidth =
|
|
||||||
typeof className === "string" && className.includes("max-w-");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogPortal data-slot="dialog-portal">
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
data-slot="dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]",
|
|
||||||
compact
|
|
||||||
? "max-w-4xl p-4"
|
|
||||||
: !hasCustomMaxWidth
|
|
||||||
? "sm:max-w-2xl p-6"
|
|
||||||
: "p-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
data-slot="dialog-close"
|
|
||||||
className={cn(
|
|
||||||
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity cursor-pointer hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
compact ? "top-2 right-3" : "top-3 right-5"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
data-slot="dialog-title"
|
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
data-slot="dialog-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
};
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent hover:bg-accent",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("ml-auto text-xs tracking-widest text-brand-400/70", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useRef, useCallback } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ImageIcon, X, Upload } from "lucide-react";
|
|
||||||
import type { ImageAttachment } from "@/store/app-store";
|
|
||||||
|
|
||||||
interface ImageDropZoneProps {
|
|
||||||
onImagesSelected: (images: ImageAttachment[]) => void;
|
|
||||||
maxFiles?: number;
|
|
||||||
maxFileSize?: number; // in bytes, default 10MB
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
images?: ImageAttachment[]; // Optional controlled images prop
|
|
||||||
}
|
|
||||||
|
|
||||||
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
|
||||||
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
||||||
|
|
||||||
export function ImageDropZone({
|
|
||||||
onImagesSelected,
|
|
||||||
maxFiles = 5,
|
|
||||||
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
disabled = false,
|
|
||||||
images,
|
|
||||||
}: ImageDropZoneProps) {
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
const [internalImages, setInternalImages] = useState<ImageAttachment[]>([]);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// Use controlled images if provided, otherwise use internal state
|
|
||||||
const selectedImages = images ?? internalImages;
|
|
||||||
|
|
||||||
// Update images - for controlled mode, just call the callback; for uncontrolled, also update internal state
|
|
||||||
const updateImages = useCallback((newImages: ImageAttachment[]) => {
|
|
||||||
if (images === undefined) {
|
|
||||||
setInternalImages(newImages);
|
|
||||||
}
|
|
||||||
onImagesSelected(newImages);
|
|
||||||
}, [images, onImagesSelected]);
|
|
||||||
|
|
||||||
const processFiles = useCallback(async (files: FileList) => {
|
|
||||||
if (disabled || isProcessing) return;
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
|
||||||
const newImages: ImageAttachment[] = [];
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
for (const file of Array.from(files)) {
|
|
||||||
// Validate file type
|
|
||||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
|
||||||
errors.push(`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file size
|
|
||||||
if (file.size > maxFileSize) {
|
|
||||||
const maxSizeMB = maxFileSize / (1024 * 1024);
|
|
||||||
errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've reached max files
|
|
||||||
if (newImages.length + selectedImages.length >= maxFiles) {
|
|
||||||
errors.push(`Maximum ${maxFiles} images allowed.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const base64 = await fileToBase64(file);
|
|
||||||
const imageAttachment: ImageAttachment = {
|
|
||||||
id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
data: base64,
|
|
||||||
mimeType: file.type,
|
|
||||||
filename: file.name,
|
|
||||||
size: file.size,
|
|
||||||
};
|
|
||||||
newImages.push(imageAttachment);
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(`${file.name}: Failed to process image.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.warn('Image upload errors:', errors);
|
|
||||||
// You could show these errors to the user via a toast or notification
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newImages.length > 0) {
|
|
||||||
const allImages = [...selectedImages, ...newImages];
|
|
||||||
updateImages(allImages);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProcessing(false);
|
|
||||||
}, [disabled, isProcessing, maxFiles, maxFileSize, selectedImages, updateImages]);
|
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
if (disabled) return;
|
|
||||||
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
if (files.length > 0) {
|
|
||||||
processFiles(files);
|
|
||||||
}
|
|
||||||
}, [disabled, processFiles]);
|
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!disabled) {
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
|
||||||
}, [disabled]);
|
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
processFiles(files);
|
|
||||||
}
|
|
||||||
// Reset the input so the same file can be selected again
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
|
||||||
}, [processFiles]);
|
|
||||||
|
|
||||||
const handleBrowseClick = useCallback(() => {
|
|
||||||
if (!disabled && fileInputRef.current) {
|
|
||||||
fileInputRef.current.click();
|
|
||||||
}
|
|
||||||
}, [disabled]);
|
|
||||||
|
|
||||||
const removeImage = useCallback((imageId: string) => {
|
|
||||||
const updated = selectedImages.filter(img => img.id !== imageId);
|
|
||||||
updateImages(updated);
|
|
||||||
}, [selectedImages, updateImages]);
|
|
||||||
|
|
||||||
const clearAllImages = useCallback(() => {
|
|
||||||
updateImages([]);
|
|
||||||
}, [updateImages]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("relative", className)}>
|
|
||||||
{/* Hidden file input */}
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
accept={ACCEPTED_IMAGE_TYPES.join(',')}
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
className="hidden"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Drop zone */}
|
|
||||||
<div
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
className={cn(
|
|
||||||
"relative rounded-lg border-2 border-dashed transition-all duration-200",
|
|
||||||
{
|
|
||||||
"border-blue-400 bg-blue-50 dark:bg-blue-950/20": isDragOver && !disabled,
|
|
||||||
"border-muted-foreground/25": !isDragOver && !disabled,
|
|
||||||
"border-muted-foreground/10 opacity-50 cursor-not-allowed": disabled,
|
|
||||||
"hover:border-blue-400 hover:bg-blue-50/50 dark:hover:bg-blue-950/10": !disabled && !isDragOver,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children || (
|
|
||||||
<div className="flex flex-col items-center justify-center p-6 text-center">
|
|
||||||
<div className={cn(
|
|
||||||
"rounded-full p-3 mb-4",
|
|
||||||
isDragOver && !disabled ? "bg-blue-100 dark:bg-blue-900/30" : "bg-muted"
|
|
||||||
)}>
|
|
||||||
{isProcessing ? (
|
|
||||||
<Upload className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ImageIcon className="h-6 w-6 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-foreground mb-1">
|
|
||||||
{isDragOver && !disabled ? "Drop your images here" : "Drag images here or click to browse"}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{maxFiles > 1 ? `Up to ${maxFiles} images` : "1 image"}, max {Math.round(maxFileSize / (1024 * 1024))}MB each
|
|
||||||
</p>
|
|
||||||
{!disabled && (
|
|
||||||
<button
|
|
||||||
onClick={handleBrowseClick}
|
|
||||||
className="mt-2 text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
||||||
disabled={isProcessing}
|
|
||||||
>
|
|
||||||
Browse files
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image previews */}
|
|
||||||
{selectedImages.length > 0 && (
|
|
||||||
<div className="mt-3 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-xs font-medium text-foreground">
|
|
||||||
{selectedImages.length} image{selectedImages.length > 1 ? 's' : ''} selected
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={clearAllImages}
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
Clear all
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{selectedImages.map((image) => (
|
|
||||||
<div
|
|
||||||
key={image.id}
|
|
||||||
className="relative group rounded-md border border-muted bg-muted/50 p-2 flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
{/* Image thumbnail */}
|
|
||||||
<div className="w-8 h-8 rounded overflow-hidden bg-muted flex-shrink-0">
|
|
||||||
<img
|
|
||||||
src={image.data}
|
|
||||||
alt={image.filename}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Image info */}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-xs font-medium text-foreground truncate">
|
|
||||||
{image.filename}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{formatFileSize(image.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* Remove button */}
|
|
||||||
{!disabled && (
|
|
||||||
<button
|
|
||||||
onClick={() => removeImage(image.id)}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-destructive hover:text-destructive-foreground text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileToBase64(file: File): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
if (typeof reader.result === 'string') {
|
|
||||||
resolve(reader.result);
|
|
||||||
} else {
|
|
||||||
reject(new Error('Failed to read file as base64'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFileSize(bytes: number): string {
|
|
||||||
if (bytes === 0) return '0 B';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-input border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
||||||
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,637 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS, parseShortcut, formatShortcut } from "@/store/app-store";
|
|
||||||
import type { KeyboardShortcuts } from "@/store/app-store";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { CheckCircle2, X, RotateCcw, Edit2 } from "lucide-react";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
|
|
||||||
// Detect if running on Mac
|
|
||||||
const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
||||||
|
|
||||||
// Keyboard layout - US QWERTY
|
|
||||||
const KEYBOARD_ROWS = [
|
|
||||||
// Number row
|
|
||||||
[
|
|
||||||
{ key: "`", label: "`", width: 1 },
|
|
||||||
{ key: "1", label: "1", width: 1 },
|
|
||||||
{ key: "2", label: "2", width: 1 },
|
|
||||||
{ key: "3", label: "3", width: 1 },
|
|
||||||
{ key: "4", label: "4", width: 1 },
|
|
||||||
{ key: "5", label: "5", width: 1 },
|
|
||||||
{ key: "6", label: "6", width: 1 },
|
|
||||||
{ key: "7", label: "7", width: 1 },
|
|
||||||
{ key: "8", label: "8", width: 1 },
|
|
||||||
{ key: "9", label: "9", width: 1 },
|
|
||||||
{ key: "0", label: "0", width: 1 },
|
|
||||||
{ key: "-", label: "-", width: 1 },
|
|
||||||
{ key: "=", label: "=", width: 1 },
|
|
||||||
],
|
|
||||||
// Top letter row
|
|
||||||
[
|
|
||||||
{ key: "Q", label: "Q", width: 1 },
|
|
||||||
{ key: "W", label: "W", width: 1 },
|
|
||||||
{ key: "E", label: "E", width: 1 },
|
|
||||||
{ key: "R", label: "R", width: 1 },
|
|
||||||
{ key: "T", label: "T", width: 1 },
|
|
||||||
{ key: "Y", label: "Y", width: 1 },
|
|
||||||
{ key: "U", label: "U", width: 1 },
|
|
||||||
{ key: "I", label: "I", width: 1 },
|
|
||||||
{ key: "O", label: "O", width: 1 },
|
|
||||||
{ key: "P", label: "P", width: 1 },
|
|
||||||
{ key: "[", label: "[", width: 1 },
|
|
||||||
{ key: "]", label: "]", width: 1 },
|
|
||||||
{ key: "\\", label: "\\", width: 1 },
|
|
||||||
],
|
|
||||||
// Home row
|
|
||||||
[
|
|
||||||
{ key: "A", label: "A", width: 1 },
|
|
||||||
{ key: "S", label: "S", width: 1 },
|
|
||||||
{ key: "D", label: "D", width: 1 },
|
|
||||||
{ key: "F", label: "F", width: 1 },
|
|
||||||
{ key: "G", label: "G", width: 1 },
|
|
||||||
{ key: "H", label: "H", width: 1 },
|
|
||||||
{ key: "J", label: "J", width: 1 },
|
|
||||||
{ key: "K", label: "K", width: 1 },
|
|
||||||
{ key: "L", label: "L", width: 1 },
|
|
||||||
{ key: ";", label: ";", width: 1 },
|
|
||||||
{ key: "'", label: "'", width: 1 },
|
|
||||||
],
|
|
||||||
// Bottom letter row
|
|
||||||
[
|
|
||||||
{ key: "Z", label: "Z", width: 1 },
|
|
||||||
{ key: "X", label: "X", width: 1 },
|
|
||||||
{ key: "C", label: "C", width: 1 },
|
|
||||||
{ key: "V", label: "V", width: 1 },
|
|
||||||
{ key: "B", label: "B", width: 1 },
|
|
||||||
{ key: "N", label: "N", width: 1 },
|
|
||||||
{ key: "M", label: "M", width: 1 },
|
|
||||||
{ key: ",", label: ",", width: 1 },
|
|
||||||
{ key: ".", label: ".", width: 1 },
|
|
||||||
{ key: "/", label: "/", width: 1 },
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Map shortcut names to human-readable labels
|
|
||||||
const SHORTCUT_LABELS: Record<keyof KeyboardShortcuts, string> = {
|
|
||||||
board: "Kanban Board",
|
|
||||||
agent: "Agent Runner",
|
|
||||||
spec: "Spec Editor",
|
|
||||||
context: "Context",
|
|
||||||
settings: "Settings",
|
|
||||||
profiles: "AI Profiles",
|
|
||||||
toggleSidebar: "Toggle Sidebar",
|
|
||||||
addFeature: "Add Feature",
|
|
||||||
addContextFile: "Add Context File",
|
|
||||||
startNext: "Start Next",
|
|
||||||
newSession: "New Session",
|
|
||||||
openProject: "Open Project",
|
|
||||||
projectPicker: "Project Picker",
|
|
||||||
cyclePrevProject: "Prev Project",
|
|
||||||
cycleNextProject: "Next Project",
|
|
||||||
addProfile: "Add Profile",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Categorize shortcuts for color coding
|
|
||||||
const SHORTCUT_CATEGORIES: Record<keyof KeyboardShortcuts, "navigation" | "ui" | "action"> = {
|
|
||||||
board: "navigation",
|
|
||||||
agent: "navigation",
|
|
||||||
spec: "navigation",
|
|
||||||
context: "navigation",
|
|
||||||
settings: "navigation",
|
|
||||||
profiles: "navigation",
|
|
||||||
toggleSidebar: "ui",
|
|
||||||
addFeature: "action",
|
|
||||||
addContextFile: "action",
|
|
||||||
startNext: "action",
|
|
||||||
newSession: "action",
|
|
||||||
openProject: "action",
|
|
||||||
projectPicker: "action",
|
|
||||||
cyclePrevProject: "action",
|
|
||||||
cycleNextProject: "action",
|
|
||||||
addProfile: "action",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Category colors
|
|
||||||
const CATEGORY_COLORS = {
|
|
||||||
navigation: {
|
|
||||||
bg: "bg-blue-500/20",
|
|
||||||
border: "border-blue-500/50",
|
|
||||||
text: "text-blue-400",
|
|
||||||
label: "Navigation",
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
bg: "bg-purple-500/20",
|
|
||||||
border: "border-purple-500/50",
|
|
||||||
text: "text-purple-400",
|
|
||||||
label: "UI Controls",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
bg: "bg-green-500/20",
|
|
||||||
border: "border-green-500/50",
|
|
||||||
text: "text-green-400",
|
|
||||||
label: "Actions",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface KeyboardMapProps {
|
|
||||||
onKeySelect?: (key: string) => void;
|
|
||||||
selectedKey?: string | null;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMapProps) {
|
|
||||||
const { keyboardShortcuts } = useAppStore();
|
|
||||||
|
|
||||||
// Create a reverse map: base key -> list of shortcut names (including info about modifiers)
|
|
||||||
const keyToShortcuts = React.useMemo(() => {
|
|
||||||
const map: Record<string, Array<{ name: keyof KeyboardShortcuts; hasModifiers: boolean }>> = {};
|
|
||||||
(Object.entries(keyboardShortcuts) as [keyof KeyboardShortcuts, string][]).forEach(
|
|
||||||
([shortcutName, shortcutStr]) => {
|
|
||||||
const parsed = parseShortcut(shortcutStr);
|
|
||||||
const normalizedKey = parsed.key.toUpperCase();
|
|
||||||
const hasModifiers = !!(parsed.shift || parsed.cmdCtrl || parsed.alt);
|
|
||||||
if (!map[normalizedKey]) {
|
|
||||||
map[normalizedKey] = [];
|
|
||||||
}
|
|
||||||
map[normalizedKey].push({ name: shortcutName, hasModifiers });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return map;
|
|
||||||
}, [keyboardShortcuts]);
|
|
||||||
|
|
||||||
const renderKey = (keyDef: { key: string; label: string; width: number }) => {
|
|
||||||
const normalizedKey = keyDef.key.toUpperCase();
|
|
||||||
const shortcutInfos = keyToShortcuts[normalizedKey] || [];
|
|
||||||
const shortcuts = shortcutInfos.map(s => s.name);
|
|
||||||
const isBound = shortcuts.length > 0;
|
|
||||||
const isSelected = selectedKey?.toUpperCase() === normalizedKey;
|
|
||||||
const isModified = shortcuts.some(
|
|
||||||
(s) => keyboardShortcuts[s] !== DEFAULT_KEYBOARD_SHORTCUTS[s]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get category for coloring (use first shortcut's category if multiple)
|
|
||||||
const category = shortcuts.length > 0 ? SHORTCUT_CATEGORIES[shortcuts[0]] : null;
|
|
||||||
const colors = category ? CATEGORY_COLORS[category] : null;
|
|
||||||
|
|
||||||
const keyElement = (
|
|
||||||
<button
|
|
||||||
key={keyDef.key}
|
|
||||||
onClick={() => onKeySelect?.(keyDef.key)}
|
|
||||||
className={cn(
|
|
||||||
"relative flex flex-col items-center justify-center rounded-lg border transition-all",
|
|
||||||
"h-12 min-w-11 py-1",
|
|
||||||
keyDef.width > 1 && `w-[${keyDef.width * 2.75}rem]`,
|
|
||||||
// Base styles
|
|
||||||
!isBound && "bg-sidebar-accent/10 border-sidebar-border hover:bg-sidebar-accent/20",
|
|
||||||
// Bound key styles
|
|
||||||
isBound && colors && `${colors.bg} ${colors.border} hover:brightness-110`,
|
|
||||||
// Selected state
|
|
||||||
isSelected && "ring-2 ring-brand-500 ring-offset-2 ring-offset-background",
|
|
||||||
// Modified indicator
|
|
||||||
isModified && "ring-1 ring-yellow-500/50"
|
|
||||||
)}
|
|
||||||
data-testid={`keyboard-key-${keyDef.key}`}
|
|
||||||
>
|
|
||||||
{/* Key label - always at top */}
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-sm font-mono font-bold leading-none",
|
|
||||||
isBound && colors ? colors.text : "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{keyDef.label}
|
|
||||||
</span>
|
|
||||||
{/* Shortcut label - always takes up space to maintain consistent height */}
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-[9px] leading-tight text-center px-0.5 truncate max-w-full h-3 mt-0.5",
|
|
||||||
isBound && shortcuts.length > 0
|
|
||||||
? (colors ? colors.text : "text-muted-foreground")
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isBound && shortcuts.length > 0
|
|
||||||
? (shortcuts.length === 1
|
|
||||||
? SHORTCUT_LABELS[shortcuts[0]].split(" ")[0]
|
|
||||||
: `${shortcuts.length}x`)
|
|
||||||
: "\u00A0" // Non-breaking space to maintain height
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
{isModified && (
|
|
||||||
<span className="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-yellow-500" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wrap in tooltip if bound
|
|
||||||
if (isBound) {
|
|
||||||
return (
|
|
||||||
<Tooltip key={keyDef.key}>
|
|
||||||
<TooltipTrigger asChild>{keyElement}</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-xs">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{shortcuts.map((shortcut) => {
|
|
||||||
const shortcutStr = keyboardShortcuts[shortcut];
|
|
||||||
const displayShortcut = formatShortcut(shortcutStr, true);
|
|
||||||
return (
|
|
||||||
<div key={shortcut} className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"w-2 h-2 rounded-full",
|
|
||||||
CATEGORY_COLORS[SHORTCUT_CATEGORIES[shortcut]].bg.replace("/20", "")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{SHORTCUT_LABELS[shortcut]}</span>
|
|
||||||
<kbd className="text-xs font-mono bg-sidebar-accent/30 px-1 rounded">
|
|
||||||
{displayShortcut}
|
|
||||||
</kbd>
|
|
||||||
{keyboardShortcuts[shortcut] !== DEFAULT_KEYBOARD_SHORTCUTS[shortcut] && (
|
|
||||||
<span className="text-xs text-yellow-400">(custom)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className={cn("space-y-4", className)} data-testid="keyboard-map">
|
|
||||||
{/* Legend */}
|
|
||||||
<div className="flex flex-wrap gap-4 justify-center text-xs">
|
|
||||||
{Object.entries(CATEGORY_COLORS).map(([key, colors]) => (
|
|
||||||
<div key={key} className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"w-4 h-4 rounded border",
|
|
||||||
colors.bg,
|
|
||||||
colors.border
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className={colors.text}>{colors.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 rounded bg-sidebar-accent/10 border border-sidebar-border" />
|
|
||||||
<span className="text-muted-foreground">Available</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
||||||
<span className="text-yellow-400">Modified</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Keyboard layout */}
|
|
||||||
<div className="flex flex-col items-center gap-1.5 p-4 rounded-xl bg-sidebar-accent/5 border border-sidebar-border">
|
|
||||||
{KEYBOARD_ROWS.map((row, rowIndex) => (
|
|
||||||
<div key={rowIndex} className="flex gap-1.5 justify-center">
|
|
||||||
{row.map(renderKey)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="flex justify-center gap-6 text-xs text-muted-foreground">
|
|
||||||
<span>
|
|
||||||
<strong className="text-foreground">{Object.keys(keyboardShortcuts).length}</strong> shortcuts
|
|
||||||
configured
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong className="text-foreground">
|
|
||||||
{Object.keys(keyToShortcuts).length}
|
|
||||||
</strong>{" "}
|
|
||||||
keys in use
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong className="text-foreground">
|
|
||||||
{KEYBOARD_ROWS.flat().length - Object.keys(keyToShortcuts).length}
|
|
||||||
</strong>{" "}
|
|
||||||
keys available
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full shortcut reference panel with editing capability
|
|
||||||
interface ShortcutReferencePanelProps {
|
|
||||||
editable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePanelProps) {
|
|
||||||
const { keyboardShortcuts, setKeyboardShortcut, resetKeyboardShortcuts } = useAppStore();
|
|
||||||
const [editingShortcut, setEditingShortcut] = React.useState<keyof KeyboardShortcuts | null>(null);
|
|
||||||
const [keyValue, setKeyValue] = React.useState("");
|
|
||||||
const [modifiers, setModifiers] = React.useState({ shift: false, cmdCtrl: false, alt: false });
|
|
||||||
const [shortcutError, setShortcutError] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const groupedShortcuts = React.useMemo(() => {
|
|
||||||
const groups: Record<string, Array<{ key: keyof KeyboardShortcuts; label: string; value: string }>> = {
|
|
||||||
navigation: [],
|
|
||||||
ui: [],
|
|
||||||
action: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
(Object.entries(SHORTCUT_CATEGORIES) as [keyof KeyboardShortcuts, string][]).forEach(
|
|
||||||
([shortcut, category]) => {
|
|
||||||
groups[category].push({
|
|
||||||
key: shortcut,
|
|
||||||
label: SHORTCUT_LABELS[shortcut],
|
|
||||||
value: keyboardShortcuts[shortcut],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}, [keyboardShortcuts]);
|
|
||||||
|
|
||||||
// Build the full shortcut string from key + modifiers
|
|
||||||
const buildShortcutString = React.useCallback((key: string, mods: typeof modifiers) => {
|
|
||||||
const parts: string[] = [];
|
|
||||||
if (mods.cmdCtrl) parts.push(isMac ? "Cmd" : "Ctrl");
|
|
||||||
if (mods.alt) parts.push(isMac ? "Opt" : "Alt");
|
|
||||||
if (mods.shift) parts.push("Shift");
|
|
||||||
parts.push(key.toUpperCase());
|
|
||||||
return parts.join("+");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Check for conflicts with other shortcuts
|
|
||||||
const checkConflict = React.useCallback((shortcutStr: string, currentKey: keyof KeyboardShortcuts) => {
|
|
||||||
const conflict = Object.entries(keyboardShortcuts).find(
|
|
||||||
([k, v]) => k !== currentKey && v.toUpperCase() === shortcutStr.toUpperCase()
|
|
||||||
);
|
|
||||||
return conflict ? SHORTCUT_LABELS[conflict[0] as keyof KeyboardShortcuts] : null;
|
|
||||||
}, [keyboardShortcuts]);
|
|
||||||
|
|
||||||
const handleStartEdit = (key: keyof KeyboardShortcuts) => {
|
|
||||||
const currentValue = keyboardShortcuts[key];
|
|
||||||
const parsed = parseShortcut(currentValue);
|
|
||||||
setEditingShortcut(key);
|
|
||||||
setKeyValue(parsed.key);
|
|
||||||
setModifiers({
|
|
||||||
shift: parsed.shift || false,
|
|
||||||
cmdCtrl: parsed.cmdCtrl || false,
|
|
||||||
alt: parsed.alt || false,
|
|
||||||
});
|
|
||||||
setShortcutError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveShortcut = () => {
|
|
||||||
if (!editingShortcut || shortcutError || !keyValue) return;
|
|
||||||
const shortcutStr = buildShortcutString(keyValue, modifiers);
|
|
||||||
setKeyboardShortcut(editingShortcut, shortcutStr);
|
|
||||||
setEditingShortcut(null);
|
|
||||||
setKeyValue("");
|
|
||||||
setModifiers({ shift: false, cmdCtrl: false, alt: false });
|
|
||||||
setShortcutError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelEdit = () => {
|
|
||||||
setEditingShortcut(null);
|
|
||||||
setKeyValue("");
|
|
||||||
setModifiers({ shift: false, cmdCtrl: false, alt: false });
|
|
||||||
setShortcutError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyChange = (value: string, currentKey: keyof KeyboardShortcuts) => {
|
|
||||||
setKeyValue(value);
|
|
||||||
// Check for conflicts with full shortcut string
|
|
||||||
if (!value) {
|
|
||||||
setShortcutError("Key cannot be empty");
|
|
||||||
} else {
|
|
||||||
const shortcutStr = buildShortcutString(value, modifiers);
|
|
||||||
const conflictLabel = checkConflict(shortcutStr, currentKey);
|
|
||||||
if (conflictLabel) {
|
|
||||||
setShortcutError(`Already used by "${conflictLabel}"`);
|
|
||||||
} else {
|
|
||||||
setShortcutError(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModifierChange = (modifier: keyof typeof modifiers, checked: boolean, currentKey: keyof KeyboardShortcuts) => {
|
|
||||||
// Enforce single modifier: when checking, uncheck all others (radio-button behavior)
|
|
||||||
const newModifiers = checked
|
|
||||||
? { shift: false, cmdCtrl: false, alt: false, [modifier]: true }
|
|
||||||
: { ...modifiers, [modifier]: false };
|
|
||||||
|
|
||||||
setModifiers(newModifiers);
|
|
||||||
|
|
||||||
// Recheck for conflicts
|
|
||||||
if (keyValue) {
|
|
||||||
const shortcutStr = buildShortcutString(keyValue, newModifiers);
|
|
||||||
const conflictLabel = checkConflict(shortcutStr, currentKey);
|
|
||||||
if (conflictLabel) {
|
|
||||||
setShortcutError(`Already used by "${conflictLabel}"`);
|
|
||||||
} else {
|
|
||||||
setShortcutError(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === "Enter" && !shortcutError && keyValue) {
|
|
||||||
handleSaveShortcut();
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
handleCancelEdit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetShortcut = (key: keyof KeyboardShortcuts) => {
|
|
||||||
setKeyboardShortcut(key, DEFAULT_KEYBOARD_SHORTCUTS[key]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className="space-y-4" data-testid="shortcut-reference-panel">
|
|
||||||
{editable && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => resetKeyboardShortcuts()}
|
|
||||||
className="gap-2 text-xs"
|
|
||||||
data-testid="reset-all-shortcuts-button"
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-3 h-3" />
|
|
||||||
Reset All to Defaults
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{Object.entries(groupedShortcuts).map(([category, shortcuts]) => {
|
|
||||||
const colors = CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS];
|
|
||||||
return (
|
|
||||||
<div key={category} className="space-y-2">
|
|
||||||
<h4 className={cn("text-sm font-semibold", colors.text)}>
|
|
||||||
{colors.label}
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{shortcuts.map(({ key, label, value }) => {
|
|
||||||
const isModified = keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
|
|
||||||
const isEditing = editingShortcut === key;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between p-2 rounded-lg bg-sidebar-accent/10 border transition-colors",
|
|
||||||
isEditing ? "border-brand-500" : "border-sidebar-border",
|
|
||||||
editable && !isEditing && "hover:bg-sidebar-accent/20 cursor-pointer"
|
|
||||||
)}
|
|
||||||
onClick={() => editable && !isEditing && handleStartEdit(key)}
|
|
||||||
data-testid={`shortcut-row-${key}`}
|
|
||||||
>
|
|
||||||
<span className="text-sm text-foreground">{label}</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isEditing ? (
|
|
||||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
||||||
{/* Modifier checkboxes */}
|
|
||||||
<div className="flex items-center gap-1.5 text-xs">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Checkbox
|
|
||||||
id={`mod-cmd-${key}`}
|
|
||||||
checked={modifiers.cmdCtrl}
|
|
||||||
onCheckedChange={(checked) => handleModifierChange("cmdCtrl", !!checked, key)}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
/>
|
|
||||||
<Label htmlFor={`mod-cmd-${key}`} className="text-xs text-muted-foreground cursor-pointer">
|
|
||||||
{isMac ? "⌘" : "Ctrl"}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Checkbox
|
|
||||||
id={`mod-alt-${key}`}
|
|
||||||
checked={modifiers.alt}
|
|
||||||
onCheckedChange={(checked) => handleModifierChange("alt", !!checked, key)}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
/>
|
|
||||||
<Label htmlFor={`mod-alt-${key}`} className="text-xs text-muted-foreground cursor-pointer">
|
|
||||||
{isMac ? "⌥" : "Alt"}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Checkbox
|
|
||||||
id={`mod-shift-${key}`}
|
|
||||||
checked={modifiers.shift}
|
|
||||||
onCheckedChange={(checked) => handleModifierChange("shift", !!checked, key)}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
/>
|
|
||||||
<Label htmlFor={`mod-shift-${key}`} className="text-xs text-muted-foreground cursor-pointer">
|
|
||||||
⇧
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">+</span>
|
|
||||||
<Input
|
|
||||||
value={keyValue}
|
|
||||||
onChange={(e) => handleKeyChange(e.target.value, key)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className={cn(
|
|
||||||
"w-12 h-7 text-center font-mono text-xs uppercase",
|
|
||||||
shortcutError && "border-red-500 focus-visible:ring-red-500"
|
|
||||||
)}
|
|
||||||
placeholder="Key"
|
|
||||||
maxLength={1}
|
|
||||||
autoFocus
|
|
||||||
data-testid={`edit-shortcut-input-${key}`}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 p-0 hover:bg-green-500/20 hover:text-green-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleSaveShortcut();
|
|
||||||
}}
|
|
||||||
disabled={!!shortcutError || !keyValue}
|
|
||||||
data-testid={`save-shortcut-${key}`}
|
|
||||||
>
|
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 p-0 hover:bg-red-500/20 hover:text-red-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCancelEdit();
|
|
||||||
}}
|
|
||||||
data-testid={`cancel-shortcut-${key}`}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<kbd
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1 text-xs font-mono rounded border",
|
|
||||||
colors.bg,
|
|
||||||
colors.border,
|
|
||||||
colors.text
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatShortcut(value, true)}
|
|
||||||
</kbd>
|
|
||||||
{isModified && editable && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-yellow-500/20 hover:text-yellow-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleResetShortcut(key);
|
|
||||||
}}
|
|
||||||
data-testid={`reset-shortcut-${key}`}
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">
|
|
||||||
Reset to default ({DEFAULT_KEYBOARD_SHORTCUTS[key]})
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{isModified && !editable && (
|
|
||||||
<span className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
||||||
)}
|
|
||||||
{editable && !isModified && (
|
|
||||||
<Edit2 className="w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{editingShortcut && shortcutError && SHORTCUT_CATEGORIES[editingShortcut] === category && (
|
|
||||||
<p className="text-xs text-red-400 mt-1">{shortcutError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
MessageSquare,
|
|
||||||
Wrench,
|
|
||||||
Zap,
|
|
||||||
AlertCircle,
|
|
||||||
CheckCircle2,
|
|
||||||
AlertTriangle,
|
|
||||||
Bug,
|
|
||||||
Info,
|
|
||||||
FileOutput,
|
|
||||||
Brain,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
parseLogOutput,
|
|
||||||
getLogTypeColors,
|
|
||||||
type LogEntry,
|
|
||||||
type LogEntryType,
|
|
||||||
} from "@/lib/log-parser";
|
|
||||||
|
|
||||||
interface LogViewerProps {
|
|
||||||
output: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLogIcon = (type: LogEntryType) => {
|
|
||||||
switch (type) {
|
|
||||||
case "prompt":
|
|
||||||
return <MessageSquare className="w-4 h-4" />;
|
|
||||||
case "tool_call":
|
|
||||||
return <Wrench className="w-4 h-4" />;
|
|
||||||
case "tool_result":
|
|
||||||
return <FileOutput className="w-4 h-4" />;
|
|
||||||
case "phase":
|
|
||||||
return <Zap className="w-4 h-4" />;
|
|
||||||
case "error":
|
|
||||||
return <AlertCircle className="w-4 h-4" />;
|
|
||||||
case "success":
|
|
||||||
return <CheckCircle2 className="w-4 h-4" />;
|
|
||||||
case "warning":
|
|
||||||
return <AlertTriangle className="w-4 h-4" />;
|
|
||||||
case "thinking":
|
|
||||||
return <Brain className="w-4 h-4" />;
|
|
||||||
case "debug":
|
|
||||||
return <Bug className="w-4 h-4" />;
|
|
||||||
default:
|
|
||||||
return <Info className="w-4 h-4" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LogEntryItemProps {
|
|
||||||
entry: LogEntry;
|
|
||||||
isExpanded: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
|
|
||||||
const colors = getLogTypeColors(entry.type);
|
|
||||||
const hasContent = entry.content.length > 100;
|
|
||||||
|
|
||||||
// Format content - detect and highlight JSON
|
|
||||||
const formattedContent = useMemo(() => {
|
|
||||||
const content = entry.content;
|
|
||||||
|
|
||||||
// Try to find and format JSON blocks
|
|
||||||
const jsonRegex = /(\{[\s\S]*?\}|\[[\s\S]*?\])/g;
|
|
||||||
let lastIndex = 0;
|
|
||||||
const parts: { type: "text" | "json"; content: string }[] = [];
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = jsonRegex.exec(content)) !== null) {
|
|
||||||
// Add text before JSON
|
|
||||||
if (match.index > lastIndex) {
|
|
||||||
parts.push({
|
|
||||||
type: "text",
|
|
||||||
content: content.slice(lastIndex, match.index),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse and format JSON
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(match[1]);
|
|
||||||
parts.push({
|
|
||||||
type: "json",
|
|
||||||
content: JSON.stringify(parsed, null, 2),
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Not valid JSON, treat as text
|
|
||||||
parts.push({ type: "text", content: match[1] });
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = match.index + match[1].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add remaining text
|
|
||||||
if (lastIndex < content.length) {
|
|
||||||
parts.push({ type: "text", content: content.slice(lastIndex) });
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.length > 0 ? parts : [{ type: "text" as const, content }];
|
|
||||||
}, [entry.content]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"rounded-lg border-l-4 transition-all duration-200",
|
|
||||||
colors.bg,
|
|
||||||
colors.border,
|
|
||||||
"hover:brightness-110"
|
|
||||||
)}
|
|
||||||
data-testid={`log-entry-${entry.type}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={onToggle}
|
|
||||||
className="w-full px-3 py-2 flex items-center gap-2 text-left"
|
|
||||||
data-testid={`log-entry-toggle-${entry.id}`}
|
|
||||||
>
|
|
||||||
{hasContent ? (
|
|
||||||
isExpanded ? (
|
|
||||||
<ChevronDown className="w-4 h-4 text-zinc-400 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-4 h-4 text-zinc-400 flex-shrink-0" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="w-4 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={cn("flex-shrink-0", colors.icon)}>
|
|
||||||
{getLogIcon(entry.type)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0",
|
|
||||||
colors.badge
|
|
||||||
)}
|
|
||||||
data-testid="log-entry-badge"
|
|
||||||
>
|
|
||||||
{entry.title}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-xs text-zinc-400 truncate flex-1 ml-2">
|
|
||||||
{!isExpanded &&
|
|
||||||
entry.content.slice(0, 80) +
|
|
||||||
(entry.content.length > 80 ? "..." : "")}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{(isExpanded || !hasContent) && (
|
|
||||||
<div
|
|
||||||
className="px-4 pb-3 pt-1"
|
|
||||||
data-testid={`log-entry-content-${entry.id}`}
|
|
||||||
>
|
|
||||||
<div className="font-mono text-xs space-y-1">
|
|
||||||
{formattedContent.map((part, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
{part.type === "json" ? (
|
|
||||||
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-primary">
|
|
||||||
{part.content}
|
|
||||||
</pre>
|
|
||||||
) : (
|
|
||||||
<pre
|
|
||||||
className={cn(
|
|
||||||
"whitespace-pre-wrap break-words",
|
|
||||||
colors.text
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{part.content}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LogViewer({ output, className }: LogViewerProps) {
|
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const entries = useMemo(() => parseLogOutput(output), [output]);
|
|
||||||
|
|
||||||
const toggleEntry = (id: string) => {
|
|
||||||
setExpandedIds((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(id)) {
|
|
||||||
next.delete(id);
|
|
||||||
} else {
|
|
||||||
next.add(id);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandAll = () => {
|
|
||||||
setExpandedIds(new Set(entries.map((e) => e.id)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const collapseAll = () => {
|
|
||||||
setExpandedIds(new Set());
|
|
||||||
};
|
|
||||||
|
|
||||||
if (entries.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center p-8 text-muted-foreground">
|
|
||||||
<div className="text-center">
|
|
||||||
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
||||||
<p className="text-sm">No log entries yet. Logs will appear here as the process runs.</p>
|
|
||||||
{output && output.trim() && (
|
|
||||||
<div className="mt-4 p-3 bg-zinc-900/50 rounded text-xs font-mono text-left max-h-40 overflow-auto">
|
|
||||||
<pre className="whitespace-pre-wrap">{output}</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count entries by type
|
|
||||||
const typeCounts = entries.reduce((acc, entry) => {
|
|
||||||
acc[entry.type] = (acc[entry.type] || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, number>);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("flex flex-col gap-2", className)}>
|
|
||||||
{/* Header with controls */}
|
|
||||||
<div className="flex items-center justify-between px-1" data-testid="log-viewer-header">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
{Object.entries(typeCounts).map(([type, count]) => {
|
|
||||||
const colors = getLogTypeColors(type as LogEntryType);
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={type}
|
|
||||||
className={cn(
|
|
||||||
"text-xs px-2 py-0.5 rounded-full",
|
|
||||||
colors.badge
|
|
||||||
)}
|
|
||||||
data-testid={`log-type-count-${type}`}
|
|
||||||
>
|
|
||||||
{type}: {count}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
onClick={expandAll}
|
|
||||||
className="text-xs text-zinc-400 hover:text-zinc-200 px-2 py-1 rounded hover:bg-zinc-800/50 transition-colors"
|
|
||||||
data-testid="log-expand-all"
|
|
||||||
>
|
|
||||||
Expand All
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={collapseAll}
|
|
||||||
className="text-xs text-zinc-400 hover:text-zinc-200 px-2 py-1 rounded hover:bg-zinc-800/50 transition-colors"
|
|
||||||
data-testid="log-collapse-all"
|
|
||||||
>
|
|
||||||
Collapse All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Log entries */}
|
|
||||||
<div className="space-y-2" data-testid="log-entries-container">
|
|
||||||
{entries.map((entry) => (
|
|
||||||
<LogEntryItem
|
|
||||||
key={entry.id}
|
|
||||||
entry={entry}
|
|
||||||
isExpanded={expandedIds.has(entry.id)}
|
|
||||||
onToggle={() => toggleEntry(entry.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface MarkdownProps {
|
|
||||||
children: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable Markdown component for rendering markdown content
|
|
||||||
* Theme-aware styling that adapts to all predefined themes
|
|
||||||
*/
|
|
||||||
export function Markdown({ children, className }: MarkdownProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"prose prose-sm prose-invert max-w-none",
|
|
||||||
// Headings
|
|
||||||
"[&_h1]:text-xl [&_h1]:text-foreground [&_h1]:font-semibold [&_h1]:mt-4 [&_h1]:mb-2",
|
|
||||||
"[&_h2]:text-lg [&_h2]:text-foreground [&_h2]:font-semibold [&_h2]:mt-4 [&_h2]:mb-2",
|
|
||||||
"[&_h3]:text-base [&_h3]:text-foreground [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-2",
|
|
||||||
"[&_h4]:text-sm [&_h4]:text-foreground [&_h4]:font-semibold [&_h4]:mt-2 [&_h4]:mb-1",
|
|
||||||
// Paragraphs
|
|
||||||
"[&_p]:text-foreground-secondary [&_p]:leading-relaxed [&_p]:my-2",
|
|
||||||
// Lists
|
|
||||||
"[&_ul]:my-2 [&_ul]:pl-4 [&_ol]:my-2 [&_ol]:pl-4",
|
|
||||||
"[&_li]:text-foreground-secondary [&_li]:my-0.5",
|
|
||||||
// Code
|
|
||||||
"[&_code]:text-chart-2 [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm",
|
|
||||||
"[&_pre]:bg-card [&_pre]:border [&_pre]:border-border [&_pre]:rounded-lg [&_pre]:my-2 [&_pre]:p-3 [&_pre]:overflow-x-auto",
|
|
||||||
"[&_pre_code]:bg-transparent [&_pre_code]:p-0",
|
|
||||||
// Strong/Bold
|
|
||||||
"[&_strong]:text-foreground [&_strong]:font-semibold",
|
|
||||||
// Links
|
|
||||||
"[&_a]:text-brand-500 [&_a]:no-underline hover:[&_a]:underline",
|
|
||||||
// Blockquotes
|
|
||||||
"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground [&_blockquote]:italic [&_blockquote]:my-2",
|
|
||||||
// Horizontal rules
|
|
||||||
"[&_hr]:border-border [&_hr]:my-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ReactMarkdown>{children}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Popover({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverContent({
|
|
||||||
className,
|
|
||||||
align = "center",
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
data-slot="popover-content"
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverAnchor({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Overlay
|
|
||||||
data-slot="sheet-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
side = "right",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content
|
|
||||||
data-slot="sheet-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
||||||
side === "right" &&
|
|
||||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
||||||
side === "left" &&
|
|
||||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
||||||
side === "top" &&
|
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
|
||||||
side === "bottom" &&
|
|
||||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
|
||||||
<XIcon className="size-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-header"
|
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
data-slot="sheet-title"
|
|
||||||
className={cn("text-foreground font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
data-slot="sheet-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
|
|
||||||
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
));
|
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Slider };
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Tabs({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
data-slot="tabs-list"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px] border border-border",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
data-slot="tabs-trigger"
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 cursor-pointer",
|
|
||||||
"text-foreground/70 hover:text-foreground hover:bg-accent",
|
|
||||||
"data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-md data-[state=active]:border-primary/50",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:outline-1",
|
|
||||||
"disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed",
|
|
||||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
data-slot="textarea"
|
|
||||||
className={cn(
|
|
||||||
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Textarea }
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
))
|
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRef, useCallback, useMemo } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface XmlSyntaxEditorProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
"data-testid"?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tokenize XML content into parts for highlighting
|
|
||||||
interface Token {
|
|
||||||
type:
|
|
||||||
| "tag-bracket"
|
|
||||||
| "tag-name"
|
|
||||||
| "attribute-name"
|
|
||||||
| "attribute-equals"
|
|
||||||
| "attribute-value"
|
|
||||||
| "text"
|
|
||||||
| "comment"
|
|
||||||
| "cdata"
|
|
||||||
| "doctype";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeXml(text: string): Token[] {
|
|
||||||
const tokens: Token[] = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (i < text.length) {
|
|
||||||
// Comment: <!-- ... -->
|
|
||||||
if (text.slice(i, i + 4) === "<!--") {
|
|
||||||
const end = text.indexOf("-->", i + 4);
|
|
||||||
if (end !== -1) {
|
|
||||||
tokens.push({ type: "comment", value: text.slice(i, end + 3) });
|
|
||||||
i = end + 3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CDATA: <![CDATA[ ... ]]>
|
|
||||||
if (text.slice(i, i + 9) === "<![CDATA[") {
|
|
||||||
const end = text.indexOf("]]>", i + 9);
|
|
||||||
if (end !== -1) {
|
|
||||||
tokens.push({ type: "cdata", value: text.slice(i, end + 3) });
|
|
||||||
i = end + 3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOCTYPE: <!DOCTYPE ... >
|
|
||||||
if (text.slice(i, i + 9).toUpperCase() === "<!DOCTYPE") {
|
|
||||||
const end = text.indexOf(">", i + 9);
|
|
||||||
if (end !== -1) {
|
|
||||||
tokens.push({ type: "doctype", value: text.slice(i, end + 1) });
|
|
||||||
i = end + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag: < ... >
|
|
||||||
if (text[i] === "<") {
|
|
||||||
// Find the end of the tag
|
|
||||||
let tagEnd = i + 1;
|
|
||||||
let inString: string | null = null;
|
|
||||||
|
|
||||||
while (tagEnd < text.length) {
|
|
||||||
const char = text[tagEnd];
|
|
||||||
|
|
||||||
if (inString) {
|
|
||||||
if (char === inString && text[tagEnd - 1] !== "\\") {
|
|
||||||
inString = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (char === '"' || char === "'") {
|
|
||||||
inString = char;
|
|
||||||
} else if (char === ">") {
|
|
||||||
tagEnd++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tagEnd++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagContent = text.slice(i, tagEnd);
|
|
||||||
const tagTokens = tokenizeTag(tagContent);
|
|
||||||
tokens.push(...tagTokens);
|
|
||||||
i = tagEnd;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text content between tags
|
|
||||||
const nextTag = text.indexOf("<", i);
|
|
||||||
if (nextTag === -1) {
|
|
||||||
tokens.push({ type: "text", value: text.slice(i) });
|
|
||||||
break;
|
|
||||||
} else if (nextTag > i) {
|
|
||||||
tokens.push({ type: "text", value: text.slice(i, nextTag) });
|
|
||||||
i = nextTag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeTag(tag: string): Token[] {
|
|
||||||
const tokens: Token[] = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
// Opening bracket (< or </ or <?)
|
|
||||||
if (tag.startsWith("</")) {
|
|
||||||
tokens.push({ type: "tag-bracket", value: "</" });
|
|
||||||
i = 2;
|
|
||||||
} else if (tag.startsWith("<?")) {
|
|
||||||
tokens.push({ type: "tag-bracket", value: "<?" });
|
|
||||||
i = 2;
|
|
||||||
} else {
|
|
||||||
tokens.push({ type: "tag-bracket", value: "<" });
|
|
||||||
i = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip whitespace
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
tokens.push({ type: "text", value: tag[i] });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag name
|
|
||||||
let tagName = "";
|
|
||||||
while (i < tag.length && /[a-zA-Z0-9_:-]/.test(tag[i])) {
|
|
||||||
tagName += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (tagName) {
|
|
||||||
tokens.push({ type: "tag-name", value: tagName });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attributes and closing
|
|
||||||
while (i < tag.length) {
|
|
||||||
// Skip whitespace
|
|
||||||
if (/\s/.test(tag[i])) {
|
|
||||||
let ws = "";
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
ws += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
tokens.push({ type: "text", value: ws });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closing bracket
|
|
||||||
if (tag[i] === ">" || tag.slice(i, i + 2) === "/>" || tag.slice(i, i + 2) === "?>") {
|
|
||||||
tokens.push({ type: "tag-bracket", value: tag.slice(i) });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute name
|
|
||||||
let attrName = "";
|
|
||||||
while (i < tag.length && /[a-zA-Z0-9_:-]/.test(tag[i])) {
|
|
||||||
attrName += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (attrName) {
|
|
||||||
tokens.push({ type: "attribute-name", value: attrName });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip whitespace around =
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
tokens.push({ type: "text", value: tag[i] });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals sign
|
|
||||||
if (tag[i] === "=") {
|
|
||||||
tokens.push({ type: "attribute-equals", value: "=" });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip whitespace after =
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
tokens.push({ type: "text", value: tag[i] });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute value
|
|
||||||
if (tag[i] === '"' || tag[i] === "'") {
|
|
||||||
const quote = tag[i];
|
|
||||||
let value = quote;
|
|
||||||
i++;
|
|
||||||
while (i < tag.length && tag[i] !== quote) {
|
|
||||||
value += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (i < tag.length) {
|
|
||||||
value += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
tokens.push({ type: "attribute-value", value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function XmlSyntaxEditor({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
className,
|
|
||||||
"data-testid": testId,
|
|
||||||
}: XmlSyntaxEditorProps) {
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const highlightRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Sync scroll between textarea and highlight layer
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (textareaRef.current && highlightRef.current) {
|
|
||||||
highlightRef.current.scrollTop = textareaRef.current.scrollTop;
|
|
||||||
highlightRef.current.scrollLeft = textareaRef.current.scrollLeft;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle tab key for indentation
|
|
||||||
const handleKeyDown = useCallback(
|
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === "Tab") {
|
|
||||||
e.preventDefault();
|
|
||||||
const textarea = e.currentTarget;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const newValue =
|
|
||||||
value.substring(0, start) + " " + value.substring(end);
|
|
||||||
onChange(newValue);
|
|
||||||
// Reset cursor position after state update
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[value, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize the highlighted content
|
|
||||||
const highlightedContent = useMemo(() => {
|
|
||||||
const tokens = tokenizeXml(value);
|
|
||||||
|
|
||||||
return tokens.map((token, index) => {
|
|
||||||
const className = `xml-${token.type}`;
|
|
||||||
// React handles escaping automatically, just render the raw value
|
|
||||||
return (
|
|
||||||
<span key={index} className={className}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("relative w-full h-full xml-editor", className)}>
|
|
||||||
{/* Syntax highlighted layer (read-only, behind textarea) */}
|
|
||||||
<div
|
|
||||||
ref={highlightRef}
|
|
||||||
className="absolute inset-0 overflow-auto pointer-events-none font-mono text-sm p-4 whitespace-pre-wrap break-words"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{value ? (
|
|
||||||
<code className="xml-highlight">{highlightedContent}</code>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground opacity-50">{placeholder}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actual textarea (transparent text, handles input) */}
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder=""
|
|
||||||
spellCheck={false}
|
|
||||||
className="absolute inset-0 w-full h-full font-mono text-sm p-4 bg-transparent resize-none focus:outline-none text-transparent caret-foreground selection:bg-primary/30"
|
|
||||||
data-testid={testId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Loader2, List, FileText, GitBranch } from "lucide-react";
|
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
|
||||||
import { LogViewer } from "@/components/ui/log-viewer";
|
|
||||||
import { GitDiffPanel } from "@/components/ui/git-diff-panel";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import type { AutoModeEvent } from "@/types/electron";
|
|
||||||
|
|
||||||
interface AgentOutputModalProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
featureDescription: string;
|
|
||||||
featureId: string;
|
|
||||||
/** The status of the feature - used to determine if spinner should be shown */
|
|
||||||
featureStatus?: string;
|
|
||||||
/** Called when a number key (0-9) is pressed while the modal is open */
|
|
||||||
onNumberKeyPress?: (key: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ViewMode = "parsed" | "raw" | "changes";
|
|
||||||
|
|
||||||
export function AgentOutputModal({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
featureDescription,
|
|
||||||
featureId,
|
|
||||||
featureStatus,
|
|
||||||
onNumberKeyPress,
|
|
||||||
}: AgentOutputModalProps) {
|
|
||||||
const [output, setOutput] = useState<string>("");
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("parsed");
|
|
||||||
const [projectPath, setProjectPath] = useState<string>("");
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const autoScrollRef = useRef(true);
|
|
||||||
const projectPathRef = useRef<string>("");
|
|
||||||
const useWorktrees = useAppStore((state) => state.useWorktrees);
|
|
||||||
|
|
||||||
// Auto-scroll to bottom when output changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (autoScrollRef.current && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [output]);
|
|
||||||
|
|
||||||
// Load existing output from file
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
const loadOutput = async () => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get current project path from store (we'll need to pass this)
|
|
||||||
const currentProject = (window as any).__currentProject;
|
|
||||||
if (!currentProject?.path) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
projectPathRef.current = currentProject.path;
|
|
||||||
setProjectPath(currentProject.path);
|
|
||||||
|
|
||||||
// Use features API to get agent output
|
|
||||||
if (api.features) {
|
|
||||||
const result = await api.features.getAgentOutput(
|
|
||||||
currentProject.path,
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setOutput(result.content || "");
|
|
||||||
} else {
|
|
||||||
setOutput("");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setOutput("");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load output:", error);
|
|
||||||
setOutput("");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadOutput();
|
|
||||||
}, [open, featureId]);
|
|
||||||
|
|
||||||
// Save output to file
|
|
||||||
const saveOutput = async (newContent: string) => {
|
|
||||||
if (!projectPathRef.current) return;
|
|
||||||
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use features API - agent output is stored in features/{id}/agent-output.md
|
|
||||||
// We need to write it directly since there's no updateAgentOutput method
|
|
||||||
// The context-manager handles this on the backend, but for frontend edits we write directly
|
|
||||||
const outputPath = `${projectPathRef.current}/.automaker/features/${featureId}/agent-output.md`;
|
|
||||||
await api.writeFile(outputPath, newContent);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to save output:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen to auto mode events and update output
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.autoMode) return;
|
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event) => {
|
|
||||||
// Filter events for this specific feature only (skip events without featureId)
|
|
||||||
if ("featureId" in event && event.featureId !== featureId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newContent = "";
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case "auto_mode_progress":
|
|
||||||
newContent = event.content || "";
|
|
||||||
break;
|
|
||||||
case "auto_mode_tool":
|
|
||||||
const toolName = event.tool || "Unknown Tool";
|
|
||||||
const toolInput = event.input
|
|
||||||
? JSON.stringify(event.input, null, 2)
|
|
||||||
: "";
|
|
||||||
newContent = `\n🔧 Tool: ${toolName}\n${
|
|
||||||
toolInput ? `Input: ${toolInput}` : ""
|
|
||||||
}`;
|
|
||||||
break;
|
|
||||||
case "auto_mode_phase":
|
|
||||||
const phaseEmoji =
|
|
||||||
event.phase === "planning"
|
|
||||||
? "📋"
|
|
||||||
: event.phase === "action"
|
|
||||||
? "⚡"
|
|
||||||
: "✅";
|
|
||||||
newContent = `\n${phaseEmoji} ${event.message}\n`;
|
|
||||||
break;
|
|
||||||
case "auto_mode_error":
|
|
||||||
newContent = `\n❌ Error: ${event.error}\n`;
|
|
||||||
break;
|
|
||||||
case "auto_mode_ultrathink_preparation":
|
|
||||||
// Format thinking level preparation information
|
|
||||||
let prepContent = `\n🧠 Ultrathink Preparation\n`;
|
|
||||||
|
|
||||||
if (event.warnings && event.warnings.length > 0) {
|
|
||||||
prepContent += `\n⚠️ Warnings:\n`;
|
|
||||||
event.warnings.forEach((warning: string) => {
|
|
||||||
prepContent += ` • ${warning}\n`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.recommendations && event.recommendations.length > 0) {
|
|
||||||
prepContent += `\n💡 Recommendations:\n`;
|
|
||||||
event.recommendations.forEach((rec: string) => {
|
|
||||||
prepContent += ` • ${rec}\n`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.estimatedCost !== undefined) {
|
|
||||||
prepContent += `\n💰 Estimated Cost: ~$${event.estimatedCost.toFixed(
|
|
||||||
2
|
|
||||||
)} per execution\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.estimatedTime) {
|
|
||||||
prepContent += `\n⏱️ Estimated Time: ${event.estimatedTime}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
newContent = prepContent;
|
|
||||||
break;
|
|
||||||
case "auto_mode_feature_complete":
|
|
||||||
const emoji = event.passes ? "✅" : "⚠️";
|
|
||||||
newContent = `\n${emoji} Task completed: ${event.message}\n`;
|
|
||||||
|
|
||||||
// Close the modal when the feature is verified (passes = true)
|
|
||||||
if (event.passes) {
|
|
||||||
// Small delay to show the completion message before closing
|
|
||||||
setTimeout(() => {
|
|
||||||
onClose();
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newContent) {
|
|
||||||
setOutput((prev) => {
|
|
||||||
const updated = prev + newContent;
|
|
||||||
saveOutput(updated);
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [open, featureId]);
|
|
||||||
|
|
||||||
// Handle scroll to detect if user scrolled up
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
||||||
autoScrollRef.current = isAtBottom;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle number key presses while modal is open
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open || !onNumberKeyPress) return;
|
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
// Check if a number key (0-9) was pressed without modifiers
|
|
||||||
if (
|
|
||||||
!event.ctrlKey &&
|
|
||||||
!event.altKey &&
|
|
||||||
!event.metaKey &&
|
|
||||||
/^[0-9]$/.test(event.key)
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
onNumberKeyPress(event.key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [open, onNumberKeyPress]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
|
||||||
<DialogContent
|
|
||||||
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
|
|
||||||
data-testid="agent-output-modal"
|
|
||||||
>
|
|
||||||
<DialogHeader className="flex-shrink-0">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
{featureStatus !== "verified" &&
|
|
||||||
featureStatus !== "waiting_approval" && (
|
|
||||||
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
|
||||||
)}
|
|
||||||
Agent Output
|
|
||||||
</DialogTitle>
|
|
||||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
|
||||||
<button
|
|
||||||
onClick={() => setViewMode("parsed")}
|
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
||||||
viewMode === "parsed"
|
|
||||||
? "bg-primary/20 text-primary shadow-sm"
|
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
||||||
}`}
|
|
||||||
data-testid="view-mode-parsed"
|
|
||||||
>
|
|
||||||
<List className="w-3.5 h-3.5" />
|
|
||||||
Logs
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setViewMode("changes")}
|
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
||||||
viewMode === "changes"
|
|
||||||
? "bg-primary/20 text-primary shadow-sm"
|
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
||||||
}`}
|
|
||||||
data-testid="view-mode-changes"
|
|
||||||
>
|
|
||||||
<GitBranch className="w-3.5 h-3.5" />
|
|
||||||
Changes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setViewMode("raw")}
|
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
||||||
viewMode === "raw"
|
|
||||||
? "bg-primary/20 text-primary shadow-sm"
|
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
||||||
}`}
|
|
||||||
data-testid="view-mode-raw"
|
|
||||||
>
|
|
||||||
<FileText className="w-3.5 h-3.5" />
|
|
||||||
Raw
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogDescription
|
|
||||||
className="mt-1 max-h-24 overflow-y-auto break-words"
|
|
||||||
data-testid="agent-output-description"
|
|
||||||
>
|
|
||||||
{featureDescription}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{viewMode === "changes" ? (
|
|
||||||
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
|
|
||||||
{projectPath ? (
|
|
||||||
<GitDiffPanel
|
|
||||||
projectPath={projectPath}
|
|
||||||
featureId={featureId}
|
|
||||||
compact={false}
|
|
||||||
useWorktrees={useWorktrees}
|
|
||||||
className="border-0 rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
||||||
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh] scrollbar-visible"
|
|
||||||
>
|
|
||||||
{isLoading && !output ? (
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
||||||
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
|
||||||
Loading output...
|
|
||||||
</div>
|
|
||||||
) : !output ? (
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
||||||
No output yet. The agent will stream output here as it works.
|
|
||||||
</div>
|
|
||||||
) : viewMode === "parsed" ? (
|
|
||||||
<LogViewer output={output} />
|
|
||||||
) : (
|
|
||||||
<div className="whitespace-pre-wrap break-words text-zinc-300">
|
|
||||||
{output}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground text-center flex-shrink-0">
|
|
||||||
{autoScrollRef.current
|
|
||||||
? "Auto-scrolling enabled"
|
|
||||||
: "Scroll to bottom to enable auto-scroll"}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,804 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { ImageDropZone } from "@/components/ui/image-drop-zone";
|
|
||||||
import {
|
|
||||||
Bot,
|
|
||||||
Send,
|
|
||||||
User,
|
|
||||||
Loader2,
|
|
||||||
Sparkles,
|
|
||||||
Wrench,
|
|
||||||
Trash2,
|
|
||||||
PanelLeftClose,
|
|
||||||
PanelLeft,
|
|
||||||
Paperclip,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useElectronAgent } from "@/hooks/use-electron-agent";
|
|
||||||
import { SessionManager } from "@/components/session-manager";
|
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
|
||||||
import type { ImageAttachment } from "@/store/app-store";
|
|
||||||
import {
|
|
||||||
useKeyboardShortcuts,
|
|
||||||
useKeyboardShortcutsConfig,
|
|
||||||
KeyboardShortcut,
|
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
|
||||||
|
|
||||||
export function AgentView() {
|
|
||||||
const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore();
|
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
|
||||||
const [input, setInput] = useState("");
|
|
||||||
const [selectedImages, setSelectedImages] = useState<ImageAttachment[]>([]);
|
|
||||||
const [showImageDropZone, setShowImageDropZone] = useState(false);
|
|
||||||
const [currentTool, setCurrentTool] = useState<string | null>(null);
|
|
||||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
|
||||||
const [showSessionManager, setShowSessionManager] = useState(true);
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
|
|
||||||
// Track if initial session has been loaded
|
|
||||||
const initialSessionLoadedRef = useRef(false);
|
|
||||||
|
|
||||||
// Scroll management for auto-scroll
|
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isUserAtBottom, setIsUserAtBottom] = useState(true);
|
|
||||||
|
|
||||||
// Input ref for auto-focus
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// Ref for quick create session function from SessionManager
|
|
||||||
const quickCreateSessionRef = useRef<(() => Promise<void>) | null>(null);
|
|
||||||
|
|
||||||
// Use the Electron agent hook (only if we have a session)
|
|
||||||
const {
|
|
||||||
messages,
|
|
||||||
isProcessing,
|
|
||||||
isConnected,
|
|
||||||
sendMessage,
|
|
||||||
clearHistory,
|
|
||||||
error: agentError,
|
|
||||||
} = useElectronAgent({
|
|
||||||
sessionId: currentSessionId || "",
|
|
||||||
workingDirectory: currentProject?.path,
|
|
||||||
onToolUse: (toolName) => {
|
|
||||||
setCurrentTool(toolName);
|
|
||||||
setTimeout(() => setCurrentTool(null), 2000);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle session selection with persistence
|
|
||||||
const handleSelectSession = useCallback((sessionId: string | null) => {
|
|
||||||
setCurrentSessionId(sessionId);
|
|
||||||
// Persist the selection for this project
|
|
||||||
if (currentProject?.path) {
|
|
||||||
setLastSelectedSession(currentProject.path, sessionId);
|
|
||||||
}
|
|
||||||
}, [currentProject?.path, setLastSelectedSession]);
|
|
||||||
|
|
||||||
// Restore last selected session when switching to Agent view or when project changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentProject?.path) {
|
|
||||||
// No project, reset
|
|
||||||
setCurrentSessionId(null);
|
|
||||||
initialSessionLoadedRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only restore once per project
|
|
||||||
if (initialSessionLoadedRef.current) return;
|
|
||||||
initialSessionLoadedRef.current = true;
|
|
||||||
|
|
||||||
const lastSessionId = getLastSelectedSession(currentProject.path);
|
|
||||||
if (lastSessionId) {
|
|
||||||
console.log("[AgentView] Restoring last selected session:", lastSessionId);
|
|
||||||
setCurrentSessionId(lastSessionId);
|
|
||||||
}
|
|
||||||
}, [currentProject?.path, getLastSelectedSession]);
|
|
||||||
|
|
||||||
// Reset initialSessionLoadedRef when project changes
|
|
||||||
useEffect(() => {
|
|
||||||
initialSessionLoadedRef.current = false;
|
|
||||||
}, [currentProject?.path]);
|
|
||||||
|
|
||||||
const handleSend = useCallback(async () => {
|
|
||||||
if ((!input.trim() && selectedImages.length === 0) || isProcessing) return;
|
|
||||||
|
|
||||||
const messageContent = input;
|
|
||||||
const messageImages = selectedImages;
|
|
||||||
|
|
||||||
setInput("");
|
|
||||||
setSelectedImages([]);
|
|
||||||
setShowImageDropZone(false);
|
|
||||||
|
|
||||||
await sendMessage(messageContent, messageImages);
|
|
||||||
}, [input, selectedImages, isProcessing, sendMessage]);
|
|
||||||
|
|
||||||
const handleImagesSelected = useCallback((images: ImageAttachment[]) => {
|
|
||||||
setSelectedImages(images);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleImageDropZone = useCallback(() => {
|
|
||||||
setShowImageDropZone(!showImageDropZone);
|
|
||||||
}, [showImageDropZone]);
|
|
||||||
|
|
||||||
// Helper function to convert file to base64
|
|
||||||
const fileToBase64 = useCallback((file: File): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
if (typeof reader.result === "string") {
|
|
||||||
resolve(reader.result);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to read file as base64"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Process dropped files
|
|
||||||
const processDroppedFiles = useCallback(
|
|
||||||
async (files: FileList) => {
|
|
||||||
if (isProcessing) return;
|
|
||||||
|
|
||||||
const ACCEPTED_IMAGE_TYPES = [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/jpg",
|
|
||||||
"image/png",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp",
|
|
||||||
];
|
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
||||||
const MAX_FILES = 5;
|
|
||||||
|
|
||||||
const newImages: ImageAttachment[] = [];
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
for (const file of Array.from(files)) {
|
|
||||||
// Validate file type
|
|
||||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
|
||||||
errors.push(
|
|
||||||
`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file size
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
|
||||||
const maxSizeMB = MAX_FILE_SIZE / (1024 * 1024);
|
|
||||||
errors.push(
|
|
||||||
`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've reached max files
|
|
||||||
if (newImages.length + selectedImages.length >= MAX_FILES) {
|
|
||||||
errors.push(`Maximum ${MAX_FILES} images allowed.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const base64 = await fileToBase64(file);
|
|
||||||
const imageAttachment: ImageAttachment = {
|
|
||||||
id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
data: base64,
|
|
||||||
mimeType: file.type,
|
|
||||||
filename: file.name,
|
|
||||||
size: file.size,
|
|
||||||
};
|
|
||||||
newImages.push(imageAttachment);
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(`${file.name}: Failed to process image.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.warn("Image upload errors:", errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newImages.length > 0) {
|
|
||||||
setSelectedImages((prev) => [...prev, ...newImages]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isProcessing, selectedImages, fileToBase64]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove individual image
|
|
||||||
const removeImage = useCallback((imageId: string) => {
|
|
||||||
setSelectedImages((prev) => prev.filter((img) => img.id !== imageId));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Drag and drop handlers for the input area
|
|
||||||
const handleDragEnter = useCallback(
|
|
||||||
(e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (isProcessing || !isConnected) return;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[agent-view] Drag enter types:",
|
|
||||||
Array.from(e.dataTransfer.types)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if dragged items contain files
|
|
||||||
if (e.dataTransfer.types.includes("Files")) {
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isProcessing, isConnected]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Only set dragOver to false if we're leaving the input container
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
|
||||||
const x = e.clientX;
|
|
||||||
const y = e.clientY;
|
|
||||||
|
|
||||||
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
|
|
||||||
setIsDragOver(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
async (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
if (isProcessing || !isConnected) return;
|
|
||||||
|
|
||||||
console.log("[agent-view] Drop event:", {
|
|
||||||
filesCount: e.dataTransfer.files.length,
|
|
||||||
itemsCount: e.dataTransfer.items.length,
|
|
||||||
types: Array.from(e.dataTransfer.types),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if we have files
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
console.log("[agent-view] Processing files from dataTransfer.files");
|
|
||||||
processDroppedFiles(files);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle file paths (from screenshots or other sources)
|
|
||||||
// This is common on macOS when dragging screenshots
|
|
||||||
const items = e.dataTransfer.items;
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
console.log("[agent-view] Processing items");
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
console.log(`[agent-view] Item ${i}:`, {
|
|
||||||
kind: item.kind,
|
|
||||||
type: item.type,
|
|
||||||
});
|
|
||||||
if (item.kind === "file") {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
if (file) {
|
|
||||||
console.log("[agent-view] Got file from item:", {
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
});
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
dataTransfer.items.add(file);
|
|
||||||
processDroppedFiles(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isProcessing, isConnected, processDroppedFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePaste = useCallback(
|
|
||||||
async (e: React.ClipboardEvent) => {
|
|
||||||
// Check if clipboard contains files
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
if (items) {
|
|
||||||
const files: File[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
console.log("[agent-view] Paste item:", {
|
|
||||||
kind: item.kind,
|
|
||||||
type: item.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (item.kind === "file") {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
if (file && file.type.startsWith("image/")) {
|
|
||||||
e.preventDefault(); // Prevent default paste of file path
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
|
||||||
console.log(
|
|
||||||
"[agent-view] Processing pasted image files:",
|
|
||||||
files.length
|
|
||||||
);
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
files.forEach((file) => dataTransfer.items.add(file));
|
|
||||||
await processDroppedFiles(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[processDroppedFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSend();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearChat = async () => {
|
|
||||||
if (!confirm("Are you sure you want to clear this conversation?")) return;
|
|
||||||
await clearHistory();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scroll position detection
|
|
||||||
const checkIfUserIsAtBottom = useCallback(() => {
|
|
||||||
const container = messagesContainerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const threshold = 50; // 50px threshold for "near bottom"
|
|
||||||
const isAtBottom =
|
|
||||||
container.scrollHeight - container.scrollTop - container.clientHeight <=
|
|
||||||
threshold;
|
|
||||||
|
|
||||||
setIsUserAtBottom(isAtBottom);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Scroll to bottom function
|
|
||||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
|
||||||
const container = messagesContainerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.scrollTo({
|
|
||||||
top: container.scrollHeight,
|
|
||||||
behavior: behavior,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle scroll events
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
checkIfUserIsAtBottom();
|
|
||||||
}, [checkIfUserIsAtBottom]);
|
|
||||||
|
|
||||||
// Auto-scroll effect when messages change
|
|
||||||
useEffect(() => {
|
|
||||||
// Only auto-scroll if user was already at bottom
|
|
||||||
if (isUserAtBottom && messages.length > 0) {
|
|
||||||
// Use a small delay to ensure DOM is updated
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom("smooth");
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [messages, isUserAtBottom, scrollToBottom]);
|
|
||||||
|
|
||||||
// Initial scroll to bottom when session changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentSessionId && messages.length > 0) {
|
|
||||||
// Scroll immediately without animation when switching sessions
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom("auto");
|
|
||||||
setIsUserAtBottom(true);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [currentSessionId, scrollToBottom]);
|
|
||||||
|
|
||||||
// Auto-focus input when session is selected/changed
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentSessionId && inputRef.current) {
|
|
||||||
// Small delay to ensure UI has updated
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}, [currentSessionId]);
|
|
||||||
|
|
||||||
// Keyboard shortcuts for agent view
|
|
||||||
const agentShortcuts: KeyboardShortcut[] = useMemo(() => {
|
|
||||||
const shortcutsList: KeyboardShortcut[] = [];
|
|
||||||
|
|
||||||
// New session shortcut - only when in agent view with a project
|
|
||||||
if (currentProject) {
|
|
||||||
shortcutsList.push({
|
|
||||||
key: shortcuts.newSession,
|
|
||||||
action: () => {
|
|
||||||
if (quickCreateSessionRef.current) {
|
|
||||||
quickCreateSessionRef.current();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
description: "Create new session",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return shortcutsList;
|
|
||||||
}, [currentProject, shortcuts]);
|
|
||||||
|
|
||||||
// Register keyboard shortcuts
|
|
||||||
useKeyboardShortcuts(agentShortcuts);
|
|
||||||
|
|
||||||
if (!currentProject) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex items-center justify-center"
|
|
||||||
data-testid="agent-view-no-project"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<Sparkles className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
|
||||||
<h2 className="text-xl font-semibold mb-2">No Project Selected</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Open or create a project to start working with the AI agent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show welcome message if no messages yet
|
|
||||||
const displayMessages =
|
|
||||||
messages.length === 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: "welcome",
|
|
||||||
role: "assistant" as const,
|
|
||||||
content:
|
|
||||||
"Hello! I'm the Automaker Agent. I can help you build software autonomously. I can read and modify files in this project, run commands, and execute tests. What would you like to create today?",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: messages;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex overflow-hidden content-bg"
|
|
||||||
data-testid="agent-view"
|
|
||||||
>
|
|
||||||
{/* Session Manager Sidebar */}
|
|
||||||
{showSessionManager && currentProject && (
|
|
||||||
<div className="w-80 border-r flex-shrink-0">
|
|
||||||
<SessionManager
|
|
||||||
currentSessionId={currentSessionId}
|
|
||||||
onSelectSession={handleSelectSession}
|
|
||||||
projectPath={currentProject.path}
|
|
||||||
isCurrentSessionThinking={isProcessing}
|
|
||||||
onQuickCreateRef={quickCreateSessionRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Chat Area */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowSessionManager(!showSessionManager)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
{showSessionManager ? (
|
|
||||||
<PanelLeftClose className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<PanelLeft className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Bot className="w-5 h-5 text-primary" />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold">AI Agent</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{currentProject.name}
|
|
||||||
{currentSessionId && !isConnected && " · Connecting..."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status indicators & actions */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{currentTool && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
|
|
||||||
<Wrench className="w-3 h-3" />
|
|
||||||
<span>{currentTool}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{agentError && (
|
|
||||||
<span className="text-xs text-destructive">{agentError}</span>
|
|
||||||
)}
|
|
||||||
{currentSessionId && messages.length > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClearChat}
|
|
||||||
disabled={isProcessing}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 mr-1" />
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages */}
|
|
||||||
{!currentSessionId ? (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex items-center justify-center"
|
|
||||||
data-testid="no-session-placeholder"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<Bot className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" />
|
|
||||||
<h2 className="text-lg font-semibold mb-2">
|
|
||||||
No Session Selected
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Create or select a session to start chatting
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={() => setShowSessionManager(true)}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<PanelLeft className="w-4 h-4 mr-2" />
|
|
||||||
{showSessionManager ? "View" : "Show"} Sessions
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
ref={messagesContainerRef}
|
|
||||||
className="flex-1 overflow-y-auto p-4 space-y-4"
|
|
||||||
data-testid="message-list"
|
|
||||||
onScroll={handleScroll}
|
|
||||||
>
|
|
||||||
{displayMessages.map((message) => (
|
|
||||||
<div
|
|
||||||
key={message.id}
|
|
||||||
className={cn(
|
|
||||||
"flex gap-3",
|
|
||||||
message.role === "user" && "flex-row-reverse"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"w-8 h-8 rounded-full flex items-center justify-center shrink-0",
|
|
||||||
message.role === "assistant" ? "bg-primary/10" : "bg-muted"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{message.role === "assistant" ? (
|
|
||||||
<Bot className="w-4 h-4 text-primary" />
|
|
||||||
) : (
|
|
||||||
<User className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"max-w-[80%] py-0",
|
|
||||||
message.role === "user"
|
|
||||||
? "bg-transparent border border-primary text-foreground"
|
|
||||||
: "border-l-4 border-primary bg-card"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent className="px-3 py-2">
|
|
||||||
{message.role === "assistant" ? (
|
|
||||||
<Markdown className="text-sm text-primary prose-headings:text-primary prose-strong:text-primary prose-code:text-primary">
|
|
||||||
{message.content}
|
|
||||||
</Markdown>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm whitespace-pre-wrap">
|
|
||||||
{message.content}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
"text-xs mt-1",
|
|
||||||
message.role === "user"
|
|
||||||
? "text-muted-foreground"
|
|
||||||
: "text-primary/70"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{new Date(message.timestamp).toLocaleTimeString()}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{isProcessing && (
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
|
||||||
<Bot className="w-4 h-4 text-primary" />
|
|
||||||
</div>
|
|
||||||
<Card className="border-l-4 border-primary bg-card py-0">
|
|
||||||
<CardContent className="p-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
|
||||||
<span className="text-sm text-primary">
|
|
||||||
Thinking...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Input */}
|
|
||||||
{currentSessionId && (
|
|
||||||
<div className="border-t border-border p-4 space-y-3 bg-background">
|
|
||||||
{/* Image Drop Zone (when visible) */}
|
|
||||||
{showImageDropZone && (
|
|
||||||
<ImageDropZone
|
|
||||||
onImagesSelected={handleImagesSelected}
|
|
||||||
images={selectedImages}
|
|
||||||
maxFiles={5}
|
|
||||||
className="mb-3"
|
|
||||||
disabled={isProcessing || !isConnected}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Text Input and Controls - with drag and drop support */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex gap-2 transition-all duration-200 rounded-lg",
|
|
||||||
isDragOver &&
|
|
||||||
"bg-primary/10 ring-2 ring-primary ring-offset-2 ring-offset-background"
|
|
||||||
)}
|
|
||||||
onDragEnter={handleDragEnter}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<div className="flex-1 relative">
|
|
||||||
<Input
|
|
||||||
ref={inputRef}
|
|
||||||
placeholder={
|
|
||||||
isDragOver
|
|
||||||
? "Drop your images here..."
|
|
||||||
: "Describe what you want to build..."
|
|
||||||
}
|
|
||||||
value={input}
|
|
||||||
onChange={(e) => setInput(e.target.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
disabled={isProcessing || !isConnected}
|
|
||||||
data-testid="agent-input"
|
|
||||||
className={cn(
|
|
||||||
"bg-input border-border",
|
|
||||||
selectedImages.length > 0 &&
|
|
||||||
"border-primary/50 bg-primary/5",
|
|
||||||
isDragOver &&
|
|
||||||
"border-primary bg-primary/10"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{selectedImages.length > 0 && !isDragOver && (
|
|
||||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded">
|
|
||||||
{selectedImages.length} image
|
|
||||||
{selectedImages.length > 1 ? "s" : ""}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isDragOver && (
|
|
||||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded flex items-center gap-1">
|
|
||||||
<Paperclip className="w-3 h-3" />
|
|
||||||
Drop here
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Attachment Button */}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="default"
|
|
||||||
onClick={toggleImageDropZone}
|
|
||||||
disabled={isProcessing || !isConnected}
|
|
||||||
className={cn(
|
|
||||||
showImageDropZone &&
|
|
||||||
"bg-primary/20 text-primary border-primary",
|
|
||||||
selectedImages.length > 0 && "border-primary"
|
|
||||||
)}
|
|
||||||
title="Attach images"
|
|
||||||
>
|
|
||||||
<Paperclip className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Send Button */}
|
|
||||||
<Button
|
|
||||||
onClick={handleSend}
|
|
||||||
disabled={
|
|
||||||
(!input.trim() && selectedImages.length === 0) ||
|
|
||||||
isProcessing ||
|
|
||||||
!isConnected
|
|
||||||
}
|
|
||||||
data-testid="send-message"
|
|
||||||
>
|
|
||||||
<Send className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selected Images Preview */}
|
|
||||||
{selectedImages.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-xs font-medium text-foreground">
|
|
||||||
{selectedImages.length} image
|
|
||||||
{selectedImages.length > 1 ? "s" : ""} attached
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedImages([])}
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
disabled={isProcessing}
|
|
||||||
>
|
|
||||||
Clear all
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{selectedImages.map((image) => (
|
|
||||||
<div
|
|
||||||
key={image.id}
|
|
||||||
className="relative group rounded-md border border-muted bg-muted/50 p-2 flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
{/* Image thumbnail */}
|
|
||||||
<div className="w-8 h-8 rounded overflow-hidden bg-muted flex-shrink-0">
|
|
||||||
<img
|
|
||||||
src={image.data}
|
|
||||||
alt={image.filename}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Image info */}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-xs font-medium text-foreground truncate">
|
|
||||||
{image.filename}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{formatFileSize(image.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* Remove button */}
|
|
||||||
<button
|
|
||||||
onClick={() => removeImage(image.id)}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-destructive hover:text-destructive-foreground text-muted-foreground"
|
|
||||||
disabled={isProcessing}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to format file size
|
|
||||||
function formatFileSize(bytes: number): string {
|
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,734 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Plus,
|
|
||||||
RefreshCw,
|
|
||||||
FileText,
|
|
||||||
Image as ImageIcon,
|
|
||||||
Trash2,
|
|
||||||
Save,
|
|
||||||
Upload,
|
|
||||||
File,
|
|
||||||
X,
|
|
||||||
BookOpen,
|
|
||||||
EditIcon,
|
|
||||||
Eye,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
|
||||||
useKeyboardShortcuts,
|
|
||||||
useKeyboardShortcutsConfig,
|
|
||||||
KeyboardShortcut,
|
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Markdown } from "../ui/markdown";
|
|
||||||
|
|
||||||
interface ContextFile {
|
|
||||||
name: string;
|
|
||||||
type: "text" | "image";
|
|
||||||
content?: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContextView() {
|
|
||||||
const { currentProject } = useAppStore();
|
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
|
||||||
const [contextFiles, setContextFiles] = useState<ContextFile[]>([]);
|
|
||||||
const [selectedFile, setSelectedFile] = useState<ContextFile | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
|
||||||
const [editedContent, setEditedContent] = useState("");
|
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
||||||
const [newFileName, setNewFileName] = useState("");
|
|
||||||
const [newFileType, setNewFileType] = useState<"text" | "image">("text");
|
|
||||||
const [uploadedImageData, setUploadedImageData] = useState<string | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [newFileContent, setNewFileContent] = useState("");
|
|
||||||
const [isDropHovering, setIsDropHovering] = useState(false);
|
|
||||||
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
|
||||||
|
|
||||||
// Keyboard shortcuts for this view
|
|
||||||
const contextShortcuts: KeyboardShortcut[] = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
key: shortcuts.addContextFile,
|
|
||||||
action: () => setIsAddDialogOpen(true),
|
|
||||||
description: "Add new context file",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[shortcuts]
|
|
||||||
);
|
|
||||||
useKeyboardShortcuts(contextShortcuts);
|
|
||||||
|
|
||||||
// Get context directory path for user-added context files
|
|
||||||
const getContextPath = useCallback(() => {
|
|
||||||
if (!currentProject) return null;
|
|
||||||
return `${currentProject.path}/.automaker/context`;
|
|
||||||
}, [currentProject]);
|
|
||||||
|
|
||||||
const isMarkdownFile = (filename: string): boolean => {
|
|
||||||
const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
|
|
||||||
return ext === ".md" || ext === ".markdown";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine if a file is an image based on extension
|
|
||||||
const isImageFile = (filename: string): boolean => {
|
|
||||||
const imageExtensions = [
|
|
||||||
".png",
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".gif",
|
|
||||||
".webp",
|
|
||||||
".svg",
|
|
||||||
".bmp",
|
|
||||||
];
|
|
||||||
const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
|
|
||||||
return imageExtensions.includes(ext);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load context files
|
|
||||||
const loadContextFiles = useCallback(async () => {
|
|
||||||
const contextPath = getContextPath();
|
|
||||||
if (!contextPath) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
|
|
||||||
// Ensure context directory exists
|
|
||||||
await api.mkdir(contextPath);
|
|
||||||
|
|
||||||
// Read directory contents
|
|
||||||
const result = await api.readdir(contextPath);
|
|
||||||
if (result.success && result.entries) {
|
|
||||||
const files: ContextFile[] = result.entries
|
|
||||||
.filter((entry) => entry.isFile)
|
|
||||||
.map((entry) => ({
|
|
||||||
name: entry.name,
|
|
||||||
type: isImageFile(entry.name) ? "image" : "text",
|
|
||||||
path: `${contextPath}/${entry.name}`,
|
|
||||||
}));
|
|
||||||
setContextFiles(files);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load context files:", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [getContextPath]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadContextFiles();
|
|
||||||
}, [loadContextFiles]);
|
|
||||||
|
|
||||||
// Load selected file content
|
|
||||||
const loadFileContent = useCallback(async (file: ContextFile) => {
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
const result = await api.readFile(file.path);
|
|
||||||
if (result.success && result.content !== undefined) {
|
|
||||||
setEditedContent(result.content);
|
|
||||||
setSelectedFile({ ...file, content: result.content });
|
|
||||||
setHasChanges(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load file content:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Select a file
|
|
||||||
const handleSelectFile = (file: ContextFile) => {
|
|
||||||
if (hasChanges) {
|
|
||||||
// Could add a confirmation dialog here
|
|
||||||
}
|
|
||||||
loadFileContent(file);
|
|
||||||
setIsPreviewMode(isMarkdownFile(file.name));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save current file
|
|
||||||
const saveFile = async () => {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
await api.writeFile(selectedFile.path, editedContent);
|
|
||||||
setSelectedFile({ ...selectedFile, content: editedContent });
|
|
||||||
setHasChanges(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to save file:", error);
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle content change
|
|
||||||
const handleContentChange = (value: string) => {
|
|
||||||
setEditedContent(value);
|
|
||||||
setHasChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add new context file
|
|
||||||
const handleAddFile = async () => {
|
|
||||||
const contextPath = getContextPath();
|
|
||||||
if (!contextPath || !newFileName.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
let filename = newFileName.trim();
|
|
||||||
|
|
||||||
// Add default extension if not provided
|
|
||||||
if (newFileType === "text" && !filename.includes(".")) {
|
|
||||||
filename += ".md";
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = `${contextPath}/${filename}`;
|
|
||||||
|
|
||||||
if (newFileType === "image" && uploadedImageData) {
|
|
||||||
// Write image data
|
|
||||||
await api.writeFile(filePath, uploadedImageData);
|
|
||||||
} else {
|
|
||||||
// Write text file with content (or empty if no content)
|
|
||||||
await api.writeFile(filePath, newFileContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsAddDialogOpen(false);
|
|
||||||
setNewFileName("");
|
|
||||||
setNewFileType("text");
|
|
||||||
setUploadedImageData(null);
|
|
||||||
setNewFileContent("");
|
|
||||||
setIsDropHovering(false);
|
|
||||||
await loadContextFiles();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to add file:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete selected file
|
|
||||||
const handleDeleteFile = async () => {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
await api.deleteFile(selectedFile.path);
|
|
||||||
|
|
||||||
setIsDeleteDialogOpen(false);
|
|
||||||
setSelectedFile(null);
|
|
||||||
setEditedContent("");
|
|
||||||
setHasChanges(false);
|
|
||||||
await loadContextFiles();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to delete file:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle image upload
|
|
||||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
const base64 = event.target?.result as string;
|
|
||||||
setUploadedImageData(base64);
|
|
||||||
if (!newFileName) {
|
|
||||||
setNewFileName(file.name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle drag and drop for file upload
|
|
||||||
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
if (files.length === 0) return;
|
|
||||||
|
|
||||||
const contextPath = getContextPath();
|
|
||||||
if (!contextPath) return;
|
|
||||||
|
|
||||||
const api = getElectronAPI();
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (event) => {
|
|
||||||
const content = event.target?.result as string;
|
|
||||||
const filePath = `${contextPath}/${file.name}`;
|
|
||||||
await api.writeFile(filePath, content);
|
|
||||||
await loadContextFiles();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isImageFile(file.name)) {
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
} else {
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle drag and drop for .txt and .md files in the add context dialog textarea
|
|
||||||
const handleTextAreaDrop = async (
|
|
||||||
e: React.DragEvent<HTMLTextAreaElement>
|
|
||||||
) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDropHovering(false);
|
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
if (files.length === 0) return;
|
|
||||||
|
|
||||||
const file = files[0]; // Only handle the first file
|
|
||||||
const fileName = file.name.toLowerCase();
|
|
||||||
|
|
||||||
// Only accept .txt and .md files
|
|
||||||
if (!fileName.endsWith(".txt") && !fileName.endsWith(".md")) {
|
|
||||||
console.warn("Only .txt and .md files are supported for drag and drop");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
const content = event.target?.result as string;
|
|
||||||
setNewFileContent(content);
|
|
||||||
|
|
||||||
// Auto-fill filename if empty
|
|
||||||
if (!newFileName) {
|
|
||||||
setNewFileName(file.name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTextAreaDragOver = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDropHovering(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTextAreaDragLeave = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDropHovering(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!currentProject) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex items-center justify-center"
|
|
||||||
data-testid="context-view-no-project"
|
|
||||||
>
|
|
||||||
<p className="text-muted-foreground">No project selected</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex items-center justify-center"
|
|
||||||
data-testid="context-view-loading"
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
|
||||||
data-testid="context-view"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<BookOpen className="w-5 h-5 text-muted-foreground" />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold">Context Files</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Add context files to include in AI prompts
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<HotkeyButton
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsAddDialogOpen(true)}
|
|
||||||
hotkey={shortcuts.addContextFile}
|
|
||||||
hotkeyActive={false}
|
|
||||||
data-testid="add-context-file"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
Add File
|
|
||||||
</HotkeyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main content area with file list and editor */}
|
|
||||||
<div
|
|
||||||
className="flex-1 flex overflow-hidden"
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
>
|
|
||||||
{/* Left Panel - File List */}
|
|
||||||
<div className="w-64 border-r border-border flex flex-col overflow-hidden">
|
|
||||||
<div className="p-3 border-b border-border">
|
|
||||||
<h2 className="text-sm font-semibold text-muted-foreground">
|
|
||||||
Context Files ({contextFiles.length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex-1 overflow-y-auto p-2"
|
|
||||||
data-testid="context-file-list"
|
|
||||||
>
|
|
||||||
{contextFiles.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center p-4">
|
|
||||||
<Upload className="w-8 h-8 text-muted-foreground mb-2" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No context files yet.
|
|
||||||
<br />
|
|
||||||
Drop files here or click Add File.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{contextFiles.map((file) => (
|
|
||||||
<button
|
|
||||||
key={file.path}
|
|
||||||
onClick={() => handleSelectFile(file)}
|
|
||||||
className={cn(
|
|
||||||
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left transition-colors",
|
|
||||||
selectedFile?.path === file.path
|
|
||||||
? "bg-primary/20 text-foreground border border-primary/30"
|
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
||||||
)}
|
|
||||||
data-testid={`context-file-${file.name}`}
|
|
||||||
>
|
|
||||||
{file.type === "image" ? (
|
|
||||||
<ImageIcon className="w-4 h-4 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
<span className="truncate text-sm">{file.name}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Panel - Editor/Preview */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
{selectedFile ? (
|
|
||||||
<>
|
|
||||||
{/* File toolbar */}
|
|
||||||
<div className="flex items-center justify-between p-3 border-b border-border bg-card">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{selectedFile.type === "image" ? (
|
|
||||||
<ImageIcon className="w-4 h-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{selectedFile.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{selectedFile.type === "text" &&
|
|
||||||
isMarkdownFile(selectedFile.name) && (
|
|
||||||
<Button
|
|
||||||
variant={"outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
|
||||||
data-testid="toggle-preview-mode"
|
|
||||||
>
|
|
||||||
{isPreviewMode ? (
|
|
||||||
<>
|
|
||||||
<EditIcon className="w-4 h-4 mr-2" />
|
|
||||||
Edit
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Eye className="w-4 h-4 mr-2" />
|
|
||||||
Preview
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{selectedFile.type === "text" && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={saveFile}
|
|
||||||
disabled={!hasChanges || isSaving}
|
|
||||||
data-testid="save-context-file"
|
|
||||||
>
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
{isSaving ? "Saving..." : hasChanges ? "Save" : "Saved"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsDeleteDialogOpen(true)}
|
|
||||||
className="text-red-500 hover:text-red-400 hover:border-red-500/50"
|
|
||||||
data-testid="delete-context-file"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content area */}
|
|
||||||
<div className="flex-1 overflow-hidden p-4">
|
|
||||||
{selectedFile.type === "image" ? (
|
|
||||||
<div
|
|
||||||
className="h-full flex items-center justify-center bg-card rounded-lg"
|
|
||||||
data-testid="image-preview"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={editedContent}
|
|
||||||
alt={selectedFile.name}
|
|
||||||
className="max-w-full max-h-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : isPreviewMode ? (
|
|
||||||
<Card className="h-full overflow-auto p-4" data-testid="markdown-preview">
|
|
||||||
<Markdown>{editedContent}</Markdown>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card className="h-full overflow-hidden">
|
|
||||||
<textarea
|
|
||||||
className="w-full h-full p-4 font-mono text-sm bg-transparent resize-none focus:outline-none"
|
|
||||||
value={editedContent}
|
|
||||||
onChange={(e) => handleContentChange(e.target.value)}
|
|
||||||
placeholder="Enter context content here..."
|
|
||||||
spellCheck={false}
|
|
||||||
data-testid="context-editor"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<File className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
|
||||||
<p className="text-foreground-secondary">
|
|
||||||
Select a file to view or edit
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-sm mt-1">
|
|
||||||
Or drop files here to add them
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add File Dialog */}
|
|
||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
|
||||||
<DialogContent
|
|
||||||
data-testid="add-context-dialog"
|
|
||||||
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add Context File</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Add a new text or image file to the context.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant={newFileType === "text" ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setNewFileType("text")}
|
|
||||||
data-testid="add-text-type"
|
|
||||||
>
|
|
||||||
<FileText className="w-4 h-4 mr-2" />
|
|
||||||
Text
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={newFileType === "image" ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setNewFileType("image")}
|
|
||||||
data-testid="add-image-type"
|
|
||||||
>
|
|
||||||
<ImageIcon className="w-4 h-4 mr-2" />
|
|
||||||
Image
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="filename">File Name</Label>
|
|
||||||
<Input
|
|
||||||
id="filename"
|
|
||||||
value={newFileName}
|
|
||||||
onChange={(e) => setNewFileName(e.target.value)}
|
|
||||||
placeholder={
|
|
||||||
newFileType === "text" ? "context.md" : "image.png"
|
|
||||||
}
|
|
||||||
data-testid="new-file-name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{newFileType === "text" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="context-content">Context Content</Label>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative rounded-lg transition-colors",
|
|
||||||
isDropHovering && "ring-2 ring-primary"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
id="context-content"
|
|
||||||
value={newFileContent}
|
|
||||||
onChange={(e) => setNewFileContent(e.target.value)}
|
|
||||||
onDrop={handleTextAreaDrop}
|
|
||||||
onDragOver={handleTextAreaDragOver}
|
|
||||||
onDragLeave={handleTextAreaDragLeave}
|
|
||||||
placeholder="Enter context content here or drag & drop a .txt or .md file..."
|
|
||||||
className={cn(
|
|
||||||
"w-full h-40 p-3 font-mono text-sm bg-background border border-border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent",
|
|
||||||
isDropHovering && "border-primary bg-primary/10"
|
|
||||||
)}
|
|
||||||
spellCheck={false}
|
|
||||||
data-testid="new-file-content"
|
|
||||||
/>
|
|
||||||
{isDropHovering && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-primary/20 rounded-lg pointer-events-none">
|
|
||||||
<div className="flex flex-col items-center text-primary">
|
|
||||||
<Upload className="w-8 h-8 mb-2" />
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
Drop .txt or .md file here
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Drag & drop .txt or .md files to import their content
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{newFileType === "image" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Upload Image</Label>
|
|
||||||
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleImageUpload}
|
|
||||||
className="hidden"
|
|
||||||
id="image-upload"
|
|
||||||
data-testid="image-upload-input"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="image-upload"
|
|
||||||
className="cursor-pointer flex flex-col items-center"
|
|
||||||
>
|
|
||||||
{uploadedImageData ? (
|
|
||||||
<img
|
|
||||||
src={uploadedImageData}
|
|
||||||
alt="Preview"
|
|
||||||
className="max-w-32 max-h-32 object-contain mb-2"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Upload className="w-8 h-8 text-muted-foreground mb-2" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{uploadedImageData
|
|
||||||
? "Click to change"
|
|
||||||
: "Click to upload"}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setIsAddDialogOpen(false);
|
|
||||||
setNewFileName("");
|
|
||||||
setUploadedImageData(null);
|
|
||||||
setNewFileContent("");
|
|
||||||
setIsDropHovering(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<HotkeyButton
|
|
||||||
onClick={handleAddFile}
|
|
||||||
disabled={
|
|
||||||
!newFileName.trim() ||
|
|
||||||
(newFileType === "image" && !uploadedImageData)
|
|
||||||
}
|
|
||||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
|
||||||
hotkeyActive={isAddDialogOpen}
|
|
||||||
data-testid="confirm-add-file"
|
|
||||||
>
|
|
||||||
Add File
|
|
||||||
</HotkeyButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
|
||||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
||||||
<DialogContent data-testid="delete-context-dialog">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Delete Context File</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Are you sure you want to delete "{selectedFile?.name}"? This
|
|
||||||
action cannot be undone.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsDeleteDialogOpen(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleDeleteFile}
|
|
||||||
className="bg-red-600 hover:bg-red-700"
|
|
||||||
data-testid="confirm-delete-file"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,539 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Loader2,
|
|
||||||
Lightbulb,
|
|
||||||
Download,
|
|
||||||
StopCircle,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
RefreshCw,
|
|
||||||
Shield,
|
|
||||||
Zap,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent, SuggestionType } from "@/lib/electron";
|
|
||||||
import { useAppStore, Feature } from "@/store/app-store";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface FeatureSuggestionsDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
projectPath: string;
|
|
||||||
// Props to persist state across dialog open/close
|
|
||||||
suggestions: FeatureSuggestion[];
|
|
||||||
setSuggestions: (suggestions: FeatureSuggestion[]) => void;
|
|
||||||
isGenerating: boolean;
|
|
||||||
setIsGenerating: (generating: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration for each suggestion type
|
|
||||||
const suggestionTypeConfig: Record<SuggestionType, {
|
|
||||||
label: string;
|
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
|
||||||
description: string;
|
|
||||||
color: string;
|
|
||||||
}> = {
|
|
||||||
features: {
|
|
||||||
label: "Feature Suggestions",
|
|
||||||
icon: Lightbulb,
|
|
||||||
description: "Discover missing features and improvements",
|
|
||||||
color: "text-yellow-500",
|
|
||||||
},
|
|
||||||
refactoring: {
|
|
||||||
label: "Refactoring Suggestions",
|
|
||||||
icon: RefreshCw,
|
|
||||||
description: "Find code smells and refactoring opportunities",
|
|
||||||
color: "text-blue-500",
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
label: "Security Suggestions",
|
|
||||||
icon: Shield,
|
|
||||||
description: "Identify security vulnerabilities and issues",
|
|
||||||
color: "text-red-500",
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
label: "Performance Suggestions",
|
|
||||||
icon: Zap,
|
|
||||||
description: "Discover performance bottlenecks and optimizations",
|
|
||||||
color: "text-green-500",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FeatureSuggestionsDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
projectPath,
|
|
||||||
suggestions,
|
|
||||||
setSuggestions,
|
|
||||||
isGenerating,
|
|
||||||
setIsGenerating,
|
|
||||||
}: FeatureSuggestionsDialogProps) {
|
|
||||||
const [progress, setProgress] = useState<string[]>([]);
|
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
||||||
const [isImporting, setIsImporting] = useState(false);
|
|
||||||
const [currentSuggestionType, setCurrentSuggestionType] = useState<SuggestionType | null>(null);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const autoScrollRef = useRef(true);
|
|
||||||
|
|
||||||
const { features, setFeatures } = useAppStore();
|
|
||||||
|
|
||||||
// Initialize selectedIds when suggestions change
|
|
||||||
useEffect(() => {
|
|
||||||
if (suggestions.length > 0 && selectedIds.size === 0) {
|
|
||||||
setSelectedIds(new Set(suggestions.map((s) => s.id)));
|
|
||||||
}
|
|
||||||
}, [suggestions, selectedIds.size]);
|
|
||||||
|
|
||||||
// Auto-scroll progress when new content arrives
|
|
||||||
useEffect(() => {
|
|
||||||
if (autoScrollRef.current && scrollRef.current && isGenerating) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [progress, isGenerating]);
|
|
||||||
|
|
||||||
// Listen for suggestion events when dialog is open
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.suggestions) return;
|
|
||||||
|
|
||||||
const unsubscribe = api.suggestions.onEvent((event: SuggestionsEvent) => {
|
|
||||||
if (event.type === "suggestions_progress") {
|
|
||||||
setProgress((prev) => [...prev, event.content || ""]);
|
|
||||||
} else if (event.type === "suggestions_tool") {
|
|
||||||
const toolName = event.tool || "Unknown Tool";
|
|
||||||
setProgress((prev) => [...prev, `Using tool: ${toolName}\n`]);
|
|
||||||
} else if (event.type === "suggestions_complete") {
|
|
||||||
setIsGenerating(false);
|
|
||||||
if (event.suggestions && event.suggestions.length > 0) {
|
|
||||||
setSuggestions(event.suggestions);
|
|
||||||
// Select all by default
|
|
||||||
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
|
|
||||||
const typeLabel = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType].label.toLowerCase() : "suggestions";
|
|
||||||
toast.success(`Generated ${event.suggestions.length} ${typeLabel}!`);
|
|
||||||
} else {
|
|
||||||
toast.info("No suggestions generated. Try again.");
|
|
||||||
}
|
|
||||||
} else if (event.type === "suggestions_error") {
|
|
||||||
setIsGenerating(false);
|
|
||||||
toast.error(`Error: ${event.error}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [open, setSuggestions, setIsGenerating, currentSuggestionType]);
|
|
||||||
|
|
||||||
// Start generating suggestions for a specific type
|
|
||||||
const handleGenerate = useCallback(async (suggestionType: SuggestionType) => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.suggestions) {
|
|
||||||
toast.error("Suggestions API not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsGenerating(true);
|
|
||||||
setProgress([]);
|
|
||||||
setSuggestions([]);
|
|
||||||
setSelectedIds(new Set());
|
|
||||||
setCurrentSuggestionType(suggestionType);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await api.suggestions.generate(projectPath, suggestionType);
|
|
||||||
if (!result.success) {
|
|
||||||
toast.error(result.error || "Failed to start generation");
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to generate suggestions:", error);
|
|
||||||
toast.error("Failed to start generation");
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
}, [projectPath, setIsGenerating, setSuggestions]);
|
|
||||||
|
|
||||||
// Stop generating
|
|
||||||
const handleStop = useCallback(async () => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.suggestions) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.suggestions.stop();
|
|
||||||
setIsGenerating(false);
|
|
||||||
toast.info("Generation stopped");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to stop generation:", error);
|
|
||||||
}
|
|
||||||
}, [setIsGenerating]);
|
|
||||||
|
|
||||||
// Toggle suggestion selection
|
|
||||||
const toggleSelection = useCallback((id: string) => {
|
|
||||||
setSelectedIds((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(id)) {
|
|
||||||
next.delete(id);
|
|
||||||
} else {
|
|
||||||
next.add(id);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Toggle expand/collapse for a suggestion
|
|
||||||
const toggleExpanded = useCallback((id: string) => {
|
|
||||||
setExpandedIds((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(id)) {
|
|
||||||
next.delete(id);
|
|
||||||
} else {
|
|
||||||
next.add(id);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Select/deselect all
|
|
||||||
const toggleSelectAll = useCallback(() => {
|
|
||||||
if (selectedIds.size === suggestions.length) {
|
|
||||||
setSelectedIds(new Set());
|
|
||||||
} else {
|
|
||||||
setSelectedIds(new Set(suggestions.map((s) => s.id)));
|
|
||||||
}
|
|
||||||
}, [selectedIds.size, suggestions]);
|
|
||||||
|
|
||||||
// Import selected suggestions as features
|
|
||||||
const handleImport = useCallback(async () => {
|
|
||||||
if (selectedIds.size === 0) {
|
|
||||||
toast.warning("No suggestions selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsImporting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
const selectedSuggestions = suggestions.filter((s) =>
|
|
||||||
selectedIds.has(s.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create new features from selected suggestions
|
|
||||||
const newFeatures: Feature[] = selectedSuggestions.map((s) => ({
|
|
||||||
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
category: s.category,
|
|
||||||
description: s.description,
|
|
||||||
steps: s.steps,
|
|
||||||
status: "backlog" as const,
|
|
||||||
skipTests: true, // As specified, testing mode true
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Create each new feature using the features API
|
|
||||||
if (api.features) {
|
|
||||||
for (const feature of newFeatures) {
|
|
||||||
await api.features.create(projectPath, feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge with existing features for store update
|
|
||||||
const updatedFeatures = [...features, ...newFeatures];
|
|
||||||
|
|
||||||
// Update store
|
|
||||||
setFeatures(updatedFeatures);
|
|
||||||
|
|
||||||
toast.success(`Imported ${newFeatures.length} features to backlog!`);
|
|
||||||
|
|
||||||
// Clear suggestions after importing
|
|
||||||
setSuggestions([]);
|
|
||||||
setSelectedIds(new Set());
|
|
||||||
setProgress([]);
|
|
||||||
setCurrentSuggestionType(null);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to import features:", error);
|
|
||||||
toast.error("Failed to import features");
|
|
||||||
} finally {
|
|
||||||
setIsImporting(false);
|
|
||||||
}
|
|
||||||
}, [selectedIds, suggestions, features, setFeatures, setSuggestions, projectPath, onClose]);
|
|
||||||
|
|
||||||
// Handle scroll to detect if user scrolled up
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
||||||
autoScrollRef.current = isAtBottom;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Go back to type selection
|
|
||||||
const handleBackToSelection = useCallback(() => {
|
|
||||||
setSuggestions([]);
|
|
||||||
setSelectedIds(new Set());
|
|
||||||
setProgress([]);
|
|
||||||
setCurrentSuggestionType(null);
|
|
||||||
}, [setSuggestions]);
|
|
||||||
|
|
||||||
const hasStarted = progress.length > 0 || suggestions.length > 0;
|
|
||||||
const hasSuggestions = suggestions.length > 0;
|
|
||||||
const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
|
||||||
<DialogContent
|
|
||||||
className="w-[70vw] max-w-[70vw] max-h-[85vh] flex flex-col"
|
|
||||||
data-testid="feature-suggestions-dialog"
|
|
||||||
>
|
|
||||||
<DialogHeader className="flex-shrink-0">
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
{currentConfig ? (
|
|
||||||
<>
|
|
||||||
<currentConfig.icon className={`w-5 h-5 ${currentConfig.color}`} />
|
|
||||||
{currentConfig.label}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Lightbulb className="w-5 h-5 text-yellow-500" />
|
|
||||||
AI Suggestions
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{currentConfig
|
|
||||||
? currentConfig.description
|
|
||||||
: "Analyze your project to discover improvements. Choose a suggestion type below."}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{!hasStarted ? (
|
|
||||||
// Initial state - show suggestion type buttons
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center py-8">
|
|
||||||
<p className="text-muted-foreground text-center max-w-lg mb-8">
|
|
||||||
Our AI will analyze your project and generate actionable suggestions.
|
|
||||||
Choose what type of analysis you want to perform:
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 gap-4 w-full max-w-2xl">
|
|
||||||
{(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map(
|
|
||||||
([type, config]) => {
|
|
||||||
const Icon = config.icon;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={type}
|
|
||||||
variant="outline"
|
|
||||||
className="h-auto py-6 px-6 flex flex-col items-center gap-3 hover:border-primary/50 transition-colors"
|
|
||||||
onClick={() => handleGenerate(type)}
|
|
||||||
data-testid={`generate-${type}-btn`}
|
|
||||||
>
|
|
||||||
<Icon className={`w-8 h-8 ${config.color}`} />
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="font-semibold">{config.label.replace(" Suggestions", "")}</div>
|
|
||||||
<div className="text-xs text-muted-foreground mt-1">
|
|
||||||
{config.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isGenerating ? (
|
|
||||||
// Generating state - show progress
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
Analyzing project...
|
|
||||||
</div>
|
|
||||||
<Button variant="destructive" size="sm" onClick={handleStop}>
|
|
||||||
<StopCircle className="w-4 h-4 mr-2" />
|
|
||||||
Stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[200px] max-h-[400px]"
|
|
||||||
>
|
|
||||||
<div className="whitespace-pre-wrap break-words text-zinc-300">
|
|
||||||
{progress.join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : hasSuggestions ? (
|
|
||||||
// Results state - show suggestions list
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{suggestions.length} suggestions generated
|
|
||||||
</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={toggleSelectAll}>
|
|
||||||
{selectedIds.size === suggestions.length
|
|
||||||
? "Deselect All"
|
|
||||||
: "Select All"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{selectedIds.size} selected
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
className="flex-1 overflow-y-auto space-y-2 min-h-[200px] max-h-[400px] pr-2"
|
|
||||||
>
|
|
||||||
{suggestions.map((suggestion) => {
|
|
||||||
const isSelected = selectedIds.has(suggestion.id);
|
|
||||||
const isExpanded = expandedIds.has(suggestion.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={suggestion.id}
|
|
||||||
className={`border rounded-lg p-3 transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? "border-primary bg-primary/5"
|
|
||||||
: "border-border hover:border-primary/50"
|
|
||||||
}`}
|
|
||||||
data-testid={`suggestion-${suggestion.id}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Checkbox
|
|
||||||
id={suggestion.id}
|
|
||||||
checked={isSelected}
|
|
||||||
onCheckedChange={() => toggleSelection(suggestion.id)}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleExpanded(suggestion.id)}
|
|
||||||
className="flex items-center gap-1 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronDown className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/20 text-primary font-medium">
|
|
||||||
#{suggestion.priority}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary text-secondary-foreground">
|
|
||||||
{suggestion.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Label
|
|
||||||
htmlFor={suggestion.id}
|
|
||||||
className="text-sm font-medium cursor-pointer"
|
|
||||||
>
|
|
||||||
{suggestion.description}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="mt-3 space-y-2 text-sm">
|
|
||||||
{suggestion.reasoning && (
|
|
||||||
<p className="text-muted-foreground italic">
|
|
||||||
{suggestion.reasoning}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{suggestion.steps.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-muted-foreground mb-1">
|
|
||||||
Implementation Steps:
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc list-inside text-xs text-muted-foreground space-y-0.5">
|
|
||||||
{suggestion.steps.map((step, i) => (
|
|
||||||
<li key={i}>{step}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// No results state
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center py-8 text-center">
|
|
||||||
<p className="text-muted-foreground mb-4">
|
|
||||||
No suggestions were generated. Try running the analysis again.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleBackToSelection}>
|
|
||||||
Back to Selection
|
|
||||||
</Button>
|
|
||||||
{currentSuggestionType && (
|
|
||||||
<Button onClick={() => handleGenerate(currentSuggestionType)}>
|
|
||||||
<Lightbulb className="w-4 h-4 mr-2" />
|
|
||||||
Try Again
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0">
|
|
||||||
{hasSuggestions && (
|
|
||||||
<div className="flex gap-2 w-full justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleBackToSelection}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
{currentSuggestionType && (
|
|
||||||
<Button variant="outline" onClick={() => handleGenerate(currentSuggestionType)}>
|
|
||||||
{currentConfig && <currentConfig.icon className="w-4 h-4 mr-2" />}
|
|
||||||
Regenerate
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="ghost" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<HotkeyButton
|
|
||||||
onClick={handleImport}
|
|
||||||
disabled={selectedIds.size === 0 || isImporting}
|
|
||||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
|
||||||
hotkeyActive={open && hasSuggestions}
|
|
||||||
>
|
|
||||||
{isImporting ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
Import {selectedIds.size} Feature
|
|
||||||
{selectedIds.size !== 1 ? "s" : ""}
|
|
||||||
</HotkeyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!hasSuggestions && !isGenerating && hasStarted && (
|
|
||||||
<Button variant="ghost" onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { memo } from "react";
|
|
||||||
import { useDroppable } from "@dnd-kit/core";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
interface KanbanColumnProps {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
color: string;
|
|
||||||
count: number;
|
|
||||||
children: ReactNode;
|
|
||||||
headerAction?: ReactNode;
|
|
||||||
opacity?: number; // Opacity percentage (0-100) - only affects background
|
|
||||||
showBorder?: boolean; // Whether to show column border
|
|
||||||
hideScrollbar?: boolean; // Whether to hide the column scrollbar
|
|
||||||
}
|
|
||||||
|
|
||||||
export const KanbanColumn = memo(function KanbanColumn({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
color,
|
|
||||||
count,
|
|
||||||
children,
|
|
||||||
headerAction,
|
|
||||||
opacity = 100,
|
|
||||||
showBorder = true,
|
|
||||||
hideScrollbar = false,
|
|
||||||
}: KanbanColumnProps) {
|
|
||||||
const { setNodeRef, isOver } = useDroppable({ id });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
className={cn(
|
|
||||||
"relative flex flex-col h-full rounded-lg transition-colors w-72",
|
|
||||||
showBorder && "border border-border"
|
|
||||||
)}
|
|
||||||
data-testid={`kanban-column-${id}`}
|
|
||||||
>
|
|
||||||
{/* Background layer with opacity - only this layer is affected by opacity */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 rounded-lg backdrop-blur-sm transition-colors",
|
|
||||||
isOver ? "bg-accent" : "bg-card"
|
|
||||||
)}
|
|
||||||
style={{ opacity: opacity / 100 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Column Header - positioned above the background */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative z-10 flex items-center gap-2 p-3",
|
|
||||||
showBorder && "border-b border-border"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn("w-3 h-3 rounded-full", color)} />
|
|
||||||
<h3 className="font-medium text-sm flex-1">{title}</h3>
|
|
||||||
{headerAction}
|
|
||||||
<span className="text-xs text-muted-foreground bg-background px-2 py-0.5 rounded-full">
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Column Content - positioned above the background */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative z-10 flex-1 overflow-y-auto p-2 space-y-2",
|
|
||||||
hideScrollbar &&
|
|
||||||
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,718 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
|
||||||
import {
|
|
||||||
useAppStore,
|
|
||||||
AIProfile,
|
|
||||||
AgentModel,
|
|
||||||
ThinkingLevel,
|
|
||||||
ModelProvider,
|
|
||||||
} from "@/store/app-store";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
useKeyboardShortcuts,
|
|
||||||
useKeyboardShortcutsConfig,
|
|
||||||
KeyboardShortcut,
|
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
UserCircle,
|
|
||||||
Plus,
|
|
||||||
Pencil,
|
|
||||||
Trash2,
|
|
||||||
Brain,
|
|
||||||
Zap,
|
|
||||||
Scale,
|
|
||||||
Cpu,
|
|
||||||
Rocket,
|
|
||||||
Sparkles,
|
|
||||||
GripVertical,
|
|
||||||
Lock,
|
|
||||||
Check,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import {
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
PointerSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
closestCenter,
|
|
||||||
} from "@dnd-kit/core";
|
|
||||||
import {
|
|
||||||
SortableContext,
|
|
||||||
useSortable,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
|
||||||
|
|
||||||
// Icon mapping for profiles
|
|
||||||
const PROFILE_ICONS: Record<
|
|
||||||
string,
|
|
||||||
React.ComponentType<{ className?: string }>
|
|
||||||
> = {
|
|
||||||
Brain,
|
|
||||||
Zap,
|
|
||||||
Scale,
|
|
||||||
Cpu,
|
|
||||||
Rocket,
|
|
||||||
Sparkles,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Available icons for selection
|
|
||||||
const ICON_OPTIONS = [
|
|
||||||
{ name: "Brain", icon: Brain },
|
|
||||||
{ name: "Zap", icon: Zap },
|
|
||||||
{ name: "Scale", icon: Scale },
|
|
||||||
{ name: "Cpu", icon: Cpu },
|
|
||||||
{ name: "Rocket", icon: Rocket },
|
|
||||||
{ name: "Sparkles", icon: Sparkles },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Model options for the form
|
|
||||||
const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
|
|
||||||
{ id: "haiku", label: "Claude Haiku" },
|
|
||||||
{ id: "sonnet", label: "Claude Sonnet" },
|
|
||||||
{ id: "opus", label: "Claude Opus" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const CODEX_MODELS: { id: AgentModel; label: string }[] = [
|
|
||||||
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
|
||||||
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
|
|
||||||
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
|
|
||||||
{ id: "gpt-5.1", label: "GPT-5.1" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const THINKING_LEVELS: { id: ThinkingLevel; label: string }[] = [
|
|
||||||
{ id: "none", label: "None" },
|
|
||||||
{ id: "low", label: "Low" },
|
|
||||||
{ id: "medium", label: "Medium" },
|
|
||||||
{ id: "high", label: "High" },
|
|
||||||
{ id: "ultrathink", label: "Ultrathink" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Helper to determine provider from model
|
|
||||||
function getProviderFromModel(model: AgentModel): ModelProvider {
|
|
||||||
if (model.startsWith("gpt")) {
|
|
||||||
return "codex";
|
|
||||||
}
|
|
||||||
return "claude";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sortable Profile Card Component
|
|
||||||
function SortableProfileCard({
|
|
||||||
profile,
|
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
}: {
|
|
||||||
profile: AIProfile;
|
|
||||||
onEdit: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({ id: profile.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
|
|
||||||
const isCodex = profile.provider === "codex";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
className={cn(
|
|
||||||
"group relative flex items-start gap-4 p-4 rounded-xl border bg-card transition-all",
|
|
||||||
isDragging && "shadow-lg",
|
|
||||||
profile.isBuiltIn
|
|
||||||
? "border-border/50"
|
|
||||||
: "border-border hover:border-primary/50 hover:shadow-sm"
|
|
||||||
)}
|
|
||||||
data-testid={`profile-card-${profile.id}`}
|
|
||||||
>
|
|
||||||
{/* Drag Handle */}
|
|
||||||
<button
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
className="p-1 rounded hover:bg-accent cursor-grab active:cursor-grabbing flex-shrink-0 mt-1"
|
|
||||||
data-testid={`profile-drag-handle-${profile.id}`}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Icon */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center",
|
|
||||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{IconComponent && (
|
|
||||||
<IconComponent
|
|
||||||
className={cn(
|
|
||||||
"w-5 h-5",
|
|
||||||
isCodex ? "text-emerald-500" : "text-primary"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-foreground">{profile.name}</h3>
|
|
||||||
{profile.isBuiltIn && (
|
|
||||||
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
||||||
<Lock className="w-2.5 h-2.5" />
|
|
||||||
Built-in
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">
|
|
||||||
{profile.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-xs px-2 py-0.5 rounded-full border",
|
|
||||||
isCodex
|
|
||||||
? "border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10"
|
|
||||||
: "border-primary/30 text-primary bg-primary/10"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{profile.model}
|
|
||||||
</span>
|
|
||||||
{profile.thinkingLevel !== "none" && (
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full border border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/10">
|
|
||||||
{profile.thinkingLevel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
{!profile.isBuiltIn && (
|
|
||||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onEdit}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
data-testid={`edit-profile-${profile.id}`}
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onDelete}
|
|
||||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
||||||
data-testid={`delete-profile-${profile.id}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile Form Component
|
|
||||||
function ProfileForm({
|
|
||||||
profile,
|
|
||||||
onSave,
|
|
||||||
onCancel,
|
|
||||||
isEditing,
|
|
||||||
hotkeyActive,
|
|
||||||
}: {
|
|
||||||
profile: Partial<AIProfile>;
|
|
||||||
onSave: (profile: Omit<AIProfile, "id">) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
isEditing: boolean;
|
|
||||||
hotkeyActive: boolean;
|
|
||||||
}) {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: profile.name || "",
|
|
||||||
description: profile.description || "",
|
|
||||||
model: profile.model || ("opus" as AgentModel),
|
|
||||||
thinkingLevel: profile.thinkingLevel || ("none" as ThinkingLevel),
|
|
||||||
icon: profile.icon || "Brain",
|
|
||||||
});
|
|
||||||
|
|
||||||
const provider = getProviderFromModel(formData.model);
|
|
||||||
const supportsThinking = modelSupportsThinking(formData.model);
|
|
||||||
|
|
||||||
const handleModelChange = (model: AgentModel) => {
|
|
||||||
const newProvider = getProviderFromModel(model);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
model,
|
|
||||||
// Reset thinking level when switching to Codex (doesn't support thinking)
|
|
||||||
thinkingLevel: newProvider === "codex" ? "none" : formData.thinkingLevel,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!formData.name.trim()) {
|
|
||||||
toast.error("Please enter a profile name");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSave({
|
|
||||||
name: formData.name.trim(),
|
|
||||||
description: formData.description.trim(),
|
|
||||||
model: formData.model,
|
|
||||||
thinkingLevel: supportsThinking ? formData.thinkingLevel : "none",
|
|
||||||
provider,
|
|
||||||
isBuiltIn: false,
|
|
||||||
icon: formData.icon,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Name */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="profile-name">Profile Name</Label>
|
|
||||||
<Input
|
|
||||||
id="profile-name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
placeholder="e.g., Heavy Task, Quick Fix"
|
|
||||||
data-testid="profile-name-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="profile-description">Description</Label>
|
|
||||||
<Textarea
|
|
||||||
id="profile-description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, description: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Describe when to use this profile..."
|
|
||||||
rows={2}
|
|
||||||
data-testid="profile-description-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Icon Selection */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Icon</Label>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{ICON_OPTIONS.map(({ name, icon: Icon }) => (
|
|
||||||
<button
|
|
||||||
key={name}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setFormData({ ...formData, icon: name })}
|
|
||||||
className={cn(
|
|
||||||
"w-10 h-10 rounded-lg flex items-center justify-center border transition-colors",
|
|
||||||
formData.icon === name
|
|
||||||
? "bg-primary text-primary-foreground border-primary"
|
|
||||||
: "bg-background hover:bg-accent border-input"
|
|
||||||
)}
|
|
||||||
data-testid={`icon-select-${name}`}
|
|
||||||
>
|
|
||||||
<Icon className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Model Selection - Claude */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="flex items-center gap-2">
|
|
||||||
<Brain className="w-4 h-4 text-primary" />
|
|
||||||
Claude Models
|
|
||||||
</Label>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{CLAUDE_MODELS.map(({ id, label }) => (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleModelChange(id)}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
|
||||||
formData.model === id
|
|
||||||
? "bg-primary text-primary-foreground border-primary"
|
|
||||||
: "bg-background hover:bg-accent border-input"
|
|
||||||
)}
|
|
||||||
data-testid={`model-select-${id}`}
|
|
||||||
>
|
|
||||||
{label.replace("Claude ", "")}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Model Selection - Codex */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="flex items-center gap-2">
|
|
||||||
<Zap className="w-4 h-4 text-emerald-500" />
|
|
||||||
Codex Models
|
|
||||||
</Label>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{CODEX_MODELS.map(({ id, label }) => (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleModelChange(id)}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
|
||||||
formData.model === id
|
|
||||||
? "bg-emerald-600 text-white border-emerald-500"
|
|
||||||
: "bg-background hover:bg-accent border-input"
|
|
||||||
)}
|
|
||||||
data-testid={`model-select-${id}`}
|
|
||||||
>
|
|
||||||
{label.replace("GPT-5.1 ", "").replace("Codex ", "")}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Thinking Level - Only for Claude models */}
|
|
||||||
{supportsThinking && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="flex items-center gap-2">
|
|
||||||
<Brain className="w-4 h-4 text-amber-500" />
|
|
||||||
Thinking Level
|
|
||||||
</Label>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{THINKING_LEVELS.map(({ id, label }) => (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setFormData({ ...formData, thinkingLevel: id });
|
|
||||||
if (id === "ultrathink") {
|
|
||||||
toast.warning("Ultrathink uses extensive reasoning", {
|
|
||||||
description:
|
|
||||||
"Best for complex architecture, migrations, or deep debugging (~$0.48/task).",
|
|
||||||
duration: 4000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 min-w-[70px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
|
||||||
formData.thinkingLevel === id
|
|
||||||
? "bg-amber-500 text-white border-amber-400"
|
|
||||||
: "bg-background hover:bg-accent border-input"
|
|
||||||
)}
|
|
||||||
data-testid={`thinking-select-${id}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Higher levels give more time to reason through complex problems.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<DialogFooter className="pt-4">
|
|
||||||
<Button variant="ghost" onClick={onCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<HotkeyButton
|
|
||||||
onClick={handleSubmit}
|
|
||||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
|
||||||
hotkeyActive={hotkeyActive}
|
|
||||||
data-testid="save-profile-button"
|
|
||||||
>
|
|
||||||
{isEditing ? "Save Changes" : "Create Profile"}
|
|
||||||
</HotkeyButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProfilesView() {
|
|
||||||
const {
|
|
||||||
aiProfiles,
|
|
||||||
addAIProfile,
|
|
||||||
updateAIProfile,
|
|
||||||
removeAIProfile,
|
|
||||||
reorderAIProfiles,
|
|
||||||
} = useAppStore();
|
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
|
||||||
|
|
||||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
||||||
const [editingProfile, setEditingProfile] = useState<AIProfile | null>(null);
|
|
||||||
|
|
||||||
// Sensors for drag-and-drop
|
|
||||||
const sensors = useSensors(
|
|
||||||
useSensor(PointerSensor, {
|
|
||||||
activationConstraint: {
|
|
||||||
distance: 5,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Separate built-in and custom profiles
|
|
||||||
const builtInProfiles = useMemo(
|
|
||||||
() => aiProfiles.filter((p) => p.isBuiltIn),
|
|
||||||
[aiProfiles]
|
|
||||||
);
|
|
||||||
const customProfiles = useMemo(
|
|
||||||
() => aiProfiles.filter((p) => !p.isBuiltIn),
|
|
||||||
[aiProfiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
|
||||||
const oldIndex = aiProfiles.findIndex((p) => p.id === active.id);
|
|
||||||
const newIndex = aiProfiles.findIndex((p) => p.id === over.id);
|
|
||||||
|
|
||||||
if (oldIndex !== -1 && newIndex !== -1) {
|
|
||||||
reorderAIProfiles(oldIndex, newIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[aiProfiles, reorderAIProfiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddProfile = (profile: Omit<AIProfile, "id">) => {
|
|
||||||
addAIProfile(profile);
|
|
||||||
setShowAddDialog(false);
|
|
||||||
toast.success("Profile created", {
|
|
||||||
description: `Created "${profile.name}" profile`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateProfile = (profile: Omit<AIProfile, "id">) => {
|
|
||||||
if (editingProfile) {
|
|
||||||
updateAIProfile(editingProfile.id, profile);
|
|
||||||
setEditingProfile(null);
|
|
||||||
toast.success("Profile updated", {
|
|
||||||
description: `Updated "${profile.name}" profile`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteProfile = (profile: AIProfile) => {
|
|
||||||
if (profile.isBuiltIn) return;
|
|
||||||
|
|
||||||
removeAIProfile(profile.id);
|
|
||||||
toast.success("Profile deleted", {
|
|
||||||
description: `Deleted "${profile.name}" profile`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build keyboard shortcuts for profiles view
|
|
||||||
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
|
|
||||||
const shortcutsList: KeyboardShortcut[] = [];
|
|
||||||
|
|
||||||
// Add profile shortcut - when in profiles view
|
|
||||||
shortcutsList.push({
|
|
||||||
key: shortcuts.addProfile,
|
|
||||||
action: () => setShowAddDialog(true),
|
|
||||||
description: "Create new profile",
|
|
||||||
});
|
|
||||||
|
|
||||||
return shortcutsList;
|
|
||||||
}, [shortcuts]);
|
|
||||||
|
|
||||||
// Register keyboard shortcuts for profiles view
|
|
||||||
useKeyboardShortcuts(profilesShortcuts);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
|
||||||
data-testid="profiles-view"
|
|
||||||
>
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
|
||||||
<div className="px-8 py-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
|
||||||
<UserCircle className="w-5 h-5 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-foreground">
|
|
||||||
AI Profiles
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Create and manage model configuration presets
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HotkeyButton
|
|
||||||
onClick={() => setShowAddDialog(true)}
|
|
||||||
hotkey={shortcuts.addProfile}
|
|
||||||
hotkeyActive={false}
|
|
||||||
data-testid="add-profile-button"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
New Profile
|
|
||||||
</HotkeyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
|
||||||
<div className="max-w-4xl mx-auto space-y-8">
|
|
||||||
{/* Custom Profiles Section */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
|
||||||
Custom Profiles
|
|
||||||
</h2>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary">
|
|
||||||
{customProfiles.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{customProfiles.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-dashed border-border p-8 text-center">
|
|
||||||
<Sparkles className="w-10 h-10 text-muted-foreground mx-auto mb-3 opacity-50" />
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
No custom profiles yet. Create one to get started!
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => setShowAddDialog(true)}
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
Create Profile
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={customProfiles.map((p) => p.id)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{customProfiles.map((profile) => (
|
|
||||||
<SortableProfileCard
|
|
||||||
key={profile.id}
|
|
||||||
profile={profile}
|
|
||||||
onEdit={() => setEditingProfile(profile)}
|
|
||||||
onDelete={() => handleDeleteProfile(profile)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Built-in Profiles Section */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
|
||||||
Built-in Profiles
|
|
||||||
</h2>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
||||||
{builtInProfiles.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Pre-configured profiles for common use cases. These cannot be
|
|
||||||
edited or deleted.
|
|
||||||
</p>
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={builtInProfiles.map((p) => p.id)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{builtInProfiles.map((profile) => (
|
|
||||||
<SortableProfileCard
|
|
||||||
key={profile.id}
|
|
||||||
profile={profile}
|
|
||||||
onEdit={() => {}}
|
|
||||||
onDelete={() => {}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add Profile Dialog */}
|
|
||||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
|
||||||
<DialogContent data-testid="add-profile-dialog">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create New Profile</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Define a reusable model configuration preset.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<ProfileForm
|
|
||||||
profile={{}}
|
|
||||||
onSave={handleAddProfile}
|
|
||||||
onCancel={() => setShowAddDialog(false)}
|
|
||||||
isEditing={false}
|
|
||||||
hotkeyActive={showAddDialog}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Edit Profile Dialog */}
|
|
||||||
<Dialog
|
|
||||||
open={!!editingProfile}
|
|
||||||
onOpenChange={() => setEditingProfile(null)}
|
|
||||||
>
|
|
||||||
<DialogContent data-testid="edit-profile-dialog">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Edit Profile</DialogTitle>
|
|
||||||
<DialogDescription>Modify your profile settings.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{editingProfile && (
|
|
||||||
<ProfileForm
|
|
||||||
profile={editingProfile}
|
|
||||||
onSave={handleUpdateProfile}
|
|
||||||
onCancel={() => setEditingProfile(null)}
|
|
||||||
isEditing={true}
|
|
||||||
hotkeyActive={!!editingProfile}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Key,
|
|
||||||
Palette,
|
|
||||||
Terminal,
|
|
||||||
Atom,
|
|
||||||
FlaskConical,
|
|
||||||
Trash2,
|
|
||||||
Settings2,
|
|
||||||
Volume2,
|
|
||||||
VolumeX,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
|
|
||||||
import { useCliStatus } from "./settings-view/hooks/use-cli-status";
|
|
||||||
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
|
|
||||||
import { SettingsHeader } from "./settings-view/components/settings-header";
|
|
||||||
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
|
|
||||||
import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog";
|
|
||||||
import { SettingsNavigation } from "./settings-view/components/settings-navigation";
|
|
||||||
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
|
|
||||||
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
|
|
||||||
import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status";
|
|
||||||
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
|
||||||
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
|
||||||
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
|
||||||
import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section";
|
|
||||||
import type {
|
|
||||||
Project as SettingsProject,
|
|
||||||
Theme,
|
|
||||||
} from "./settings-view/shared/types";
|
|
||||||
import type { Project as ElectronProject } from "@/lib/electron";
|
|
||||||
|
|
||||||
// Navigation items for the side panel
|
|
||||||
const NAV_ITEMS = [
|
|
||||||
{ id: "api-keys", label: "API Keys", icon: Key },
|
|
||||||
{ id: "claude", label: "Claude", icon: Terminal },
|
|
||||||
{ id: "codex", label: "Codex", icon: Atom },
|
|
||||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
|
||||||
{ id: "audio", label: "Audio", icon: Volume2 },
|
|
||||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
|
||||||
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
|
||||||
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SettingsView() {
|
|
||||||
const {
|
|
||||||
theme,
|
|
||||||
setTheme,
|
|
||||||
setProjectTheme,
|
|
||||||
defaultSkipTests,
|
|
||||||
setDefaultSkipTests,
|
|
||||||
useWorktrees,
|
|
||||||
setUseWorktrees,
|
|
||||||
showProfilesOnly,
|
|
||||||
setShowProfilesOnly,
|
|
||||||
muteDoneSound,
|
|
||||||
setMuteDoneSound,
|
|
||||||
currentProject,
|
|
||||||
moveProjectToTrash,
|
|
||||||
} = useAppStore();
|
|
||||||
|
|
||||||
// Convert electron Project to settings-view Project type
|
|
||||||
const convertProject = (
|
|
||||||
project: ElectronProject | null
|
|
||||||
): SettingsProject | null => {
|
|
||||||
if (!project) return null;
|
|
||||||
return {
|
|
||||||
id: project.id,
|
|
||||||
name: project.name,
|
|
||||||
path: project.path,
|
|
||||||
theme: project.theme as Theme | undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsProject = convertProject(currentProject);
|
|
||||||
|
|
||||||
// Compute the effective theme for the current project
|
|
||||||
const effectiveTheme = (settingsProject?.theme || theme) as Theme;
|
|
||||||
|
|
||||||
// Handler to set theme - always updates global theme (user's preference),
|
|
||||||
// and also sets per-project theme if a project is selected
|
|
||||||
const handleSetTheme = (newTheme: typeof theme) => {
|
|
||||||
// Always update global theme so user's preference persists across all projects
|
|
||||||
setTheme(newTheme);
|
|
||||||
// Also set per-project theme if a project is selected
|
|
||||||
if (currentProject) {
|
|
||||||
setProjectTheme(currentProject.id, newTheme);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use CLI status hook
|
|
||||||
const {
|
|
||||||
claudeCliStatus,
|
|
||||||
codexCliStatus,
|
|
||||||
isCheckingClaudeCli,
|
|
||||||
isCheckingCodexCli,
|
|
||||||
handleRefreshClaudeCli,
|
|
||||||
handleRefreshCodexCli,
|
|
||||||
} = useCliStatus();
|
|
||||||
|
|
||||||
// Use scroll tracking hook
|
|
||||||
const { activeSection, scrollToSection, scrollContainerRef } =
|
|
||||||
useScrollTracking({
|
|
||||||
items: NAV_ITEMS,
|
|
||||||
filterFn: (item) => item.id !== "danger" || !!currentProject,
|
|
||||||
initialSection: "api-keys",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
||||||
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
|
||||||
data-testid="settings-view"
|
|
||||||
>
|
|
||||||
{/* Header Section */}
|
|
||||||
<SettingsHeader />
|
|
||||||
|
|
||||||
{/* Content Area with Sidebar */}
|
|
||||||
<div className="flex-1 flex overflow-hidden">
|
|
||||||
{/* Sticky Side Navigation */}
|
|
||||||
<SettingsNavigation
|
|
||||||
navItems={NAV_ITEMS}
|
|
||||||
activeSection={activeSection}
|
|
||||||
currentProject={currentProject}
|
|
||||||
onNavigate={scrollToSection}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
|
||||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
|
|
||||||
<div className="max-w-4xl mx-auto space-y-6 pb-96">
|
|
||||||
{/* API Keys Section */}
|
|
||||||
<ApiKeysSection />
|
|
||||||
|
|
||||||
{/* Claude CLI Status Section */}
|
|
||||||
{claudeCliStatus && (
|
|
||||||
<ClaudeCliStatus
|
|
||||||
status={claudeCliStatus}
|
|
||||||
isChecking={isCheckingClaudeCli}
|
|
||||||
onRefresh={handleRefreshClaudeCli}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Codex CLI Status Section */}
|
|
||||||
{codexCliStatus && (
|
|
||||||
<CodexCliStatus
|
|
||||||
status={codexCliStatus}
|
|
||||||
isChecking={isCheckingCodexCli}
|
|
||||||
onRefresh={handleRefreshCodexCli}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Appearance Section */}
|
|
||||||
<AppearanceSection
|
|
||||||
effectiveTheme={effectiveTheme}
|
|
||||||
currentProject={settingsProject}
|
|
||||||
onThemeChange={handleSetTheme}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Keyboard Shortcuts Section */}
|
|
||||||
<KeyboardShortcutsSection
|
|
||||||
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Audio Section */}
|
|
||||||
<div
|
|
||||||
id="audio"
|
|
||||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Volume2 className="w-5 h-5 text-brand-500" />
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
|
||||||
Audio
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Configure audio and notification settings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
{/* Mute Done Sound Setting */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<Checkbox
|
|
||||||
id="mute-done-sound"
|
|
||||||
checked={muteDoneSound}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setMuteDoneSound(checked === true)
|
|
||||||
}
|
|
||||||
className="mt-0.5"
|
|
||||||
data-testid="mute-done-sound-checkbox"
|
|
||||||
/>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label
|
|
||||||
htmlFor="mute-done-sound"
|
|
||||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<VolumeX className="w-4 h-4 text-brand-500" />
|
|
||||||
Mute notification sound when agents complete
|
|
||||||
</Label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
When enabled, disables the "ding" sound that
|
|
||||||
plays when an agent completes a feature. The feature
|
|
||||||
will still move to the completed column, but without
|
|
||||||
audio notification.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feature Defaults Section */}
|
|
||||||
<FeatureDefaultsSection
|
|
||||||
showProfilesOnly={showProfilesOnly}
|
|
||||||
defaultSkipTests={defaultSkipTests}
|
|
||||||
useWorktrees={useWorktrees}
|
|
||||||
onShowProfilesOnlyChange={setShowProfilesOnly}
|
|
||||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
|
||||||
onUseWorktreesChange={setUseWorktrees}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Danger Zone Section - Only show when a project is selected */}
|
|
||||||
<DangerZoneSection
|
|
||||||
project={settingsProject}
|
|
||||||
onDeleteClick={() => setShowDeleteDialog(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Keyboard Map Dialog */}
|
|
||||||
<KeyboardMapDialog
|
|
||||||
open={showKeyboardMapDialog}
|
|
||||||
onOpenChange={setShowKeyboardMapDialog}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Delete Project Confirmation Dialog */}
|
|
||||||
<DeleteProjectDialog
|
|
||||||
open={showDeleteDialog}
|
|
||||||
onOpenChange={setShowDeleteDialog}
|
|
||||||
project={currentProject}
|
|
||||||
onConfirm={moveProjectToTrash}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import { useSetupStore } from "@/store/setup-store";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Key, CheckCircle2 } from "lucide-react";
|
|
||||||
import { ApiKeyField } from "./api-key-field";
|
|
||||||
import { buildProviderConfigs } from "@/config/api-providers";
|
|
||||||
import { AuthenticationStatusDisplay } from "./authentication-status-display";
|
|
||||||
import { SecurityNotice } from "./security-notice";
|
|
||||||
import { useApiKeyManagement } from "./hooks/use-api-key-management";
|
|
||||||
|
|
||||||
export function ApiKeysSection() {
|
|
||||||
const { apiKeys } = useAppStore();
|
|
||||||
const { claudeAuthStatus, codexAuthStatus } = useSetupStore();
|
|
||||||
|
|
||||||
const { providerConfigParams, apiKeyStatus, handleSave, saved } =
|
|
||||||
useApiKeyManagement();
|
|
||||||
|
|
||||||
const providerConfigs = buildProviderConfigs(providerConfigParams);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="api-keys"
|
|
||||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Key className="w-5 h-5 text-brand-500" />
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">API Keys</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Configure your AI provider API keys. Keys are stored locally in your
|
|
||||||
browser.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* API Key Fields */}
|
|
||||||
{providerConfigs.map((provider) => (
|
|
||||||
<ApiKeyField key={provider.key} config={provider} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Authentication Status Display */}
|
|
||||||
<AuthenticationStatusDisplay
|
|
||||||
claudeAuthStatus={claudeAuthStatus}
|
|
||||||
codexAuthStatus={codexAuthStatus}
|
|
||||||
apiKeyStatus={apiKeyStatus}
|
|
||||||
apiKeys={apiKeys}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Security Notice */}
|
|
||||||
<SecurityNotice />
|
|
||||||
|
|
||||||
{/* Save Button */}
|
|
||||||
<div className="flex items-center gap-4 pt-2">
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
data-testid="save-settings"
|
|
||||||
className="min-w-[120px] bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
|
|
||||||
>
|
|
||||||
{saved ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
|
||||||
Saved!
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Save API Keys"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
CheckCircle2,
|
|
||||||
AlertCircle,
|
|
||||||
Info,
|
|
||||||
Terminal,
|
|
||||||
Atom,
|
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { ClaudeAuthStatus, CodexAuthStatus } from "@/store/setup-store";
|
|
||||||
|
|
||||||
interface AuthenticationStatusDisplayProps {
|
|
||||||
claudeAuthStatus: ClaudeAuthStatus | null;
|
|
||||||
codexAuthStatus: CodexAuthStatus | null;
|
|
||||||
apiKeyStatus: {
|
|
||||||
hasAnthropicKey: boolean;
|
|
||||||
hasOpenAIKey: boolean;
|
|
||||||
hasGoogleKey: boolean;
|
|
||||||
} | null;
|
|
||||||
apiKeys: {
|
|
||||||
anthropic: string;
|
|
||||||
google: string;
|
|
||||||
openai: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthenticationStatusDisplay({
|
|
||||||
claudeAuthStatus,
|
|
||||||
codexAuthStatus,
|
|
||||||
apiKeyStatus,
|
|
||||||
apiKeys,
|
|
||||||
}: AuthenticationStatusDisplayProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 pt-4 border-t border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<Info className="w-4 h-4 text-brand-500" />
|
|
||||||
<Label className="text-foreground font-semibold">
|
|
||||||
Current Authentication Configuration
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Claude Authentication Status */}
|
|
||||||
<div className="p-3 rounded-lg bg-card border border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
|
||||||
<Terminal className="w-4 h-4 text-brand-500" />
|
|
||||||
<span className="text-sm font-medium text-foreground">
|
|
||||||
Claude (Anthropic)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5 text-xs min-h-12">
|
|
||||||
{claudeAuthStatus?.authenticated ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
|
||||||
<span className="text-green-400 font-medium">Authenticated</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>
|
|
||||||
{claudeAuthStatus.method === "oauth_token_env"
|
|
||||||
? "Using CLAUDE_CODE_OAUTH_TOKEN"
|
|
||||||
: claudeAuthStatus.method === "oauth_token"
|
|
||||||
? "Using stored OAuth token (subscription)"
|
|
||||||
: claudeAuthStatus.method === "api_key_env"
|
|
||||||
? "Using ANTHROPIC_API_KEY"
|
|
||||||
: claudeAuthStatus.method === "api_key"
|
|
||||||
? "Using stored API key"
|
|
||||||
: claudeAuthStatus.method === "credentials_file"
|
|
||||||
? "Using credentials file"
|
|
||||||
: claudeAuthStatus.method === "cli_authenticated"
|
|
||||||
? "Using Claude CLI authentication"
|
|
||||||
: `Using ${claudeAuthStatus.method || "detected"} authentication`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : apiKeyStatus?.hasAnthropicKey ? (
|
|
||||||
<div className="flex items-center gap-2 text-blue-400">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>Using environment variable (ANTHROPIC_API_KEY)</span>
|
|
||||||
</div>
|
|
||||||
) : apiKeys.anthropic ? (
|
|
||||||
<div className="flex items-center gap-2 text-blue-400">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>Using manual API key from settings</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
|
|
||||||
<AlertCircle className="w-3 h-3 shrink-0" />
|
|
||||||
<span className="text-xs">Not configured</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Codex/OpenAI Authentication Status */}
|
|
||||||
<div className="p-3 rounded-lg bg-card border border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
|
||||||
<Atom className="w-4 h-4 text-green-500" />
|
|
||||||
<span className="text-sm font-medium text-foreground">
|
|
||||||
Codex (OpenAI)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5 text-xs min-h-12">
|
|
||||||
{codexAuthStatus?.authenticated ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
|
||||||
<span className="text-green-400 font-medium">Authenticated</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>
|
|
||||||
{codexAuthStatus.method === "subscription"
|
|
||||||
? "Using Codex subscription (Plus/Team)"
|
|
||||||
: codexAuthStatus.method === "cli_verified" ||
|
|
||||||
codexAuthStatus.method === "cli_tokens"
|
|
||||||
? "Using CLI login (OpenAI account)"
|
|
||||||
: codexAuthStatus.method === "api_key"
|
|
||||||
? "Using stored API key"
|
|
||||||
: codexAuthStatus.method === "env"
|
|
||||||
? "Using OPENAI_API_KEY"
|
|
||||||
: `Using ${codexAuthStatus.method || "unknown"} authentication`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : apiKeyStatus?.hasOpenAIKey ? (
|
|
||||||
<div className="flex items-center gap-2 text-blue-400">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>Using environment variable (OPENAI_API_KEY)</span>
|
|
||||||
</div>
|
|
||||||
) : apiKeys.openai ? (
|
|
||||||
<div className="flex items-center gap-2 text-blue-400">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>Using manual API key from settings</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
|
|
||||||
<AlertCircle className="w-3 h-3 shrink-0" />
|
|
||||||
<span className="text-xs">Not configured</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Google/Gemini Authentication Status */}
|
|
||||||
<div className="p-3 rounded-lg bg-card border border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
|
||||||
<Sparkles className="w-4 h-4 text-purple-500" />
|
|
||||||
<span className="text-sm font-medium text-foreground">
|
|
||||||
Gemini (Google)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5 text-xs min-h-12">
|
|
||||||
{apiKeyStatus?.hasGoogleKey ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
|
||||||
<span className="text-green-400 font-medium">Authenticated</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>Using GOOGLE_API_KEY</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : apiKeys.google ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
|
||||||
<span className="text-green-400 font-medium">Authenticated</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Info className="w-3 h-3 shrink-0" />
|
|
||||||
<span>Using stored API key</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
|
|
||||||
<AlertCircle className="w-3 h-3 shrink-0" />
|
|
||||||
<span className="text-xs">Not configured</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useAppStore } from "@/store/app-store";
|
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
|
||||||
import type { ProviderConfigParams } from "@/config/api-providers";
|
|
||||||
|
|
||||||
interface TestResult {
|
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiKeyStatus {
|
|
||||||
hasAnthropicKey: boolean;
|
|
||||||
hasOpenAIKey: boolean;
|
|
||||||
hasGoogleKey: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook for managing API key state and operations
|
|
||||||
* Handles input values, visibility toggles, connection testing, and saving
|
|
||||||
*/
|
|
||||||
export function useApiKeyManagement() {
|
|
||||||
const { apiKeys, setApiKeys } = useAppStore();
|
|
||||||
|
|
||||||
// API key values
|
|
||||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
|
||||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
|
||||||
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
|
|
||||||
|
|
||||||
// Visibility toggles
|
|
||||||
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
|
||||||
const [showGoogleKey, setShowGoogleKey] = useState(false);
|
|
||||||
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
|
|
||||||
|
|
||||||
// Test connection states
|
|
||||||
const [testingConnection, setTestingConnection] = useState(false);
|
|
||||||
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
|
||||||
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
|
|
||||||
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false);
|
|
||||||
const [openaiTestResult, setOpenaiTestResult] = useState<TestResult | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// API key status from environment
|
|
||||||
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
|
|
||||||
|
|
||||||
// Save state
|
|
||||||
const [saved, setSaved] = useState(false);
|
|
||||||
|
|
||||||
// Sync local state with store
|
|
||||||
useEffect(() => {
|
|
||||||
setAnthropicKey(apiKeys.anthropic);
|
|
||||||
setGoogleKey(apiKeys.google);
|
|
||||||
setOpenaiKey(apiKeys.openai);
|
|
||||||
}, [apiKeys]);
|
|
||||||
|
|
||||||
// Check API key status from environment on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const checkApiKeyStatus = async () => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (api?.setup?.getApiKeys) {
|
|
||||||
try {
|
|
||||||
const status = await api.setup.getApiKeys();
|
|
||||||
if (status.success) {
|
|
||||||
setApiKeyStatus({
|
|
||||||
hasAnthropicKey: status.hasAnthropicKey,
|
|
||||||
hasOpenAIKey: status.hasOpenAIKey,
|
|
||||||
hasGoogleKey: status.hasGoogleKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to check API key status:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkApiKeyStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Test Anthropic/Claude connection
|
|
||||||
const handleTestAnthropicConnection = async () => {
|
|
||||||
setTestingConnection(true);
|
|
||||||
setTestResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/claude/test", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ apiKey: anthropicKey }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
setTestResult({
|
|
||||||
success: true,
|
|
||||||
message: data.message || "Connection successful! Claude responded.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setTestResult({
|
|
||||||
success: false,
|
|
||||||
message: data.error || "Failed to connect to Claude API.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setTestResult({
|
|
||||||
success: false,
|
|
||||||
message: "Network error. Please check your connection.",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setTestingConnection(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test Google/Gemini connection
|
|
||||||
const handleTestGeminiConnection = async () => {
|
|
||||||
setTestingGeminiConnection(true);
|
|
||||||
setGeminiTestResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/gemini/test", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ apiKey: googleKey }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
setGeminiTestResult({
|
|
||||||
success: true,
|
|
||||||
message: data.message || "Connection successful! Gemini responded.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setGeminiTestResult({
|
|
||||||
success: false,
|
|
||||||
message: data.error || "Failed to connect to Gemini API.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setGeminiTestResult({
|
|
||||||
success: false,
|
|
||||||
message: "Network error. Please check your connection.",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setTestingGeminiConnection(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test OpenAI connection
|
|
||||||
const handleTestOpenaiConnection = async () => {
|
|
||||||
setTestingOpenaiConnection(true);
|
|
||||||
setOpenaiTestResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (api?.testOpenAIConnection) {
|
|
||||||
const result = await api.testOpenAIConnection(openaiKey);
|
|
||||||
if (result.success) {
|
|
||||||
setOpenaiTestResult({
|
|
||||||
success: true,
|
|
||||||
message:
|
|
||||||
result.message || "Connection successful! OpenAI API responded.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setOpenaiTestResult({
|
|
||||||
success: false,
|
|
||||||
message: result.error || "Failed to connect to OpenAI API.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to web API test
|
|
||||||
const response = await fetch("/api/openai/test", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ apiKey: openaiKey }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
setOpenaiTestResult({
|
|
||||||
success: true,
|
|
||||||
message:
|
|
||||||
data.message || "Connection successful! OpenAI API responded.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setOpenaiTestResult({
|
|
||||||
success: false,
|
|
||||||
message: data.error || "Failed to connect to OpenAI API.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setOpenaiTestResult({
|
|
||||||
success: false,
|
|
||||||
message: "Network error. Please check your connection.",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setTestingOpenaiConnection(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save API keys
|
|
||||||
const handleSave = () => {
|
|
||||||
setApiKeys({
|
|
||||||
anthropic: anthropicKey,
|
|
||||||
google: googleKey,
|
|
||||||
openai: openaiKey,
|
|
||||||
});
|
|
||||||
setSaved(true);
|
|
||||||
setTimeout(() => setSaved(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build provider config params for buildProviderConfigs
|
|
||||||
const providerConfigParams: ProviderConfigParams = {
|
|
||||||
apiKeys,
|
|
||||||
anthropic: {
|
|
||||||
value: anthropicKey,
|
|
||||||
setValue: setAnthropicKey,
|
|
||||||
show: showAnthropicKey,
|
|
||||||
setShow: setShowAnthropicKey,
|
|
||||||
testing: testingConnection,
|
|
||||||
onTest: handleTestAnthropicConnection,
|
|
||||||
result: testResult,
|
|
||||||
},
|
|
||||||
google: {
|
|
||||||
value: googleKey,
|
|
||||||
setValue: setGoogleKey,
|
|
||||||
show: showGoogleKey,
|
|
||||||
setShow: setShowGoogleKey,
|
|
||||||
testing: testingGeminiConnection,
|
|
||||||
onTest: handleTestGeminiConnection,
|
|
||||||
result: geminiTestResult,
|
|
||||||
},
|
|
||||||
openai: {
|
|
||||||
value: openaiKey,
|
|
||||||
setValue: setOpenaiKey,
|
|
||||||
show: showOpenaiKey,
|
|
||||||
setShow: setShowOpenaiKey,
|
|
||||||
testing: testingOpenaiConnection,
|
|
||||||
onTest: handleTestOpenaiConnection,
|
|
||||||
result: openaiTestResult,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Provider config params for buildProviderConfigs
|
|
||||||
providerConfigParams,
|
|
||||||
|
|
||||||
// API key status from environment
|
|
||||||
apiKeyStatus,
|
|
||||||
|
|
||||||
// Save handler and state
|
|
||||||
handleSave,
|
|
||||||
saved,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Palette } from "lucide-react";
|
|
||||||
import { themeOptions } from "@/config/theme-options";
|
|
||||||
import type { Theme, Project } from "../shared/types";
|
|
||||||
|
|
||||||
interface AppearanceSectionProps {
|
|
||||||
effectiveTheme: Theme;
|
|
||||||
currentProject: Project | null;
|
|
||||||
onThemeChange: (theme: Theme) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppearanceSection({
|
|
||||||
effectiveTheme,
|
|
||||||
currentProject,
|
|
||||||
onThemeChange,
|
|
||||||
}: AppearanceSectionProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="appearance"
|
|
||||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Palette className="w-5 h-5 text-brand-500" />
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">Appearance</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Customize the look and feel of your application.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label className="text-foreground">
|
|
||||||
Theme{" "}
|
|
||||||
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
|
|
||||||
</Label>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
||||||
{themeOptions.map(({ value, label, Icon, testId }) => {
|
|
||||||
const isActive = effectiveTheme === value;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={value}
|
|
||||||
variant={isActive ? "secondary" : "outline"}
|
|
||||||
onClick={() => onThemeChange(value)}
|
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
|
||||||
isActive ? "border-brand-500 ring-1 ring-brand-500/50" : ""
|
|
||||||
}`}
|
|
||||||
data-testid={testId}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4" />
|
|
||||||
<span className="font-medium text-sm">{label}</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Terminal,
|
|
||||||
CheckCircle2,
|
|
||||||
AlertCircle,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { CliStatus } from "../shared/types";
|
|
||||||
|
|
||||||
interface CliStatusProps {
|
|
||||||
status: CliStatus | null;
|
|
||||||
isChecking: boolean;
|
|
||||||
onRefresh: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ClaudeCliStatus({
|
|
||||||
status,
|
|
||||||
isChecking,
|
|
||||||
onRefresh,
|
|
||||||
}: CliStatusProps) {
|
|
||||||
if (!status) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="claude"
|
|
||||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Terminal className="w-5 h-5 text-brand-500" />
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
|
||||||
Claude Code CLI
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onRefresh}
|
|
||||||
disabled={isChecking}
|
|
||||||
data-testid="refresh-claude-cli"
|
|
||||||
title="Refresh Claude CLI detection"
|
|
||||||
>
|
|
||||||
<RefreshCw
|
|
||||||
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Claude Code CLI provides better performance for long-running tasks,
|
|
||||||
especially with ultrathink.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
{status.success && status.status === "installed" ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
||||||
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-green-400">
|
|
||||||
Claude Code CLI Installed
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-green-400/80 mt-1 space-y-1">
|
|
||||||
{status.method && (
|
|
||||||
<p>
|
|
||||||
Method: <span className="font-mono">{status.method}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{status.version && (
|
|
||||||
<p>
|
|
||||||
Version:{" "}
|
|
||||||
<span className="font-mono">{status.version}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{status.path && (
|
|
||||||
<p className="truncate" title={status.path}>
|
|
||||||
Path:{" "}
|
|
||||||
<span className="font-mono text-[10px]">
|
|
||||||
{status.path}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{status.recommendation && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{status.recommendation}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
|
||||||
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-yellow-400">
|
|
||||||
Claude Code CLI Not Detected
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-yellow-400/80 mt-1">
|
|
||||||
{status.recommendation ||
|
|
||||||
"Consider installing Claude Code CLI for optimal performance with ultrathink."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{status.installCommands && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium text-foreground-secondary">
|
|
||||||
Installation Commands:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{status.installCommands.npm && (
|
|
||||||
<div className="p-2 rounded bg-background border border-border-glass">
|
|
||||||
<p className="text-xs text-muted-foreground mb-1">npm:</p>
|
|
||||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
|
||||||
{status.installCommands.npm}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{status.installCommands.macos && (
|
|
||||||
<div className="p-2 rounded bg-background border border-border-glass">
|
|
||||||
<p className="text-xs text-muted-foreground mb-1">
|
|
||||||
macOS/Linux:
|
|
||||||
</p>
|
|
||||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
|
||||||
{status.installCommands.macos}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{status.installCommands.windows && (
|
|
||||||
<div className="p-2 rounded bg-background border border-border-glass">
|
|
||||||
<p className="text-xs text-muted-foreground mb-1">
|
|
||||||
Windows (PowerShell):
|
|
||||||
</p>
|
|
||||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
|
||||||
{status.installCommands.windows}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Terminal,
|
|
||||||
CheckCircle2,
|
|
||||||
AlertCircle,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { CliStatus } from "../shared/types";
|
|
||||||
|
|
||||||
interface CliStatusProps {
|
|
||||||
status: CliStatus | null;
|
|
||||||
isChecking: boolean;
|
|
||||||
onRefresh: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CodexCliStatus({
|
|
||||||
status,
|
|
||||||
isChecking,
|
|
||||||
onRefresh,
|
|
||||||
}: CliStatusProps) {
|
|
||||||
if (!status) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="codex"
|
|
||||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Terminal className="w-5 h-5 text-green-500" />
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
|
||||||
OpenAI Codex CLI
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onRefresh}
|
|
||||||
disabled={isChecking}
|
|
||||||
data-testid="refresh-codex-cli"
|
|
||||||
title="Refresh Codex CLI detection"
|
|
||||||
>
|
|
||||||
<RefreshCw
|
|
||||||
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
{status.success && status.status === "installed" ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
||||||
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-green-400">
|
|
||||||
Codex CLI Installed
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-green-400/80 mt-1 space-y-1">
|
|
||||||
{status.method && (
|
|
||||||
<p>
|
|
||||||
Method: <span className="font-mono">{status.method}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{status.version && (
|
|
||||||
<p>
|
|
||||||
Version:{" "}
|
|
||||||
<span className="font-mono">{status.version}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{status.path && (
|
|
||||||
<p className="truncate" title={status.path}>
|
|
||||||
Path:{" "}
|
|
||||||
<span className="font-mono text-[10px]">
|
|
||||||
{status.path}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{status.recommendation && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{status.recommendation}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : status.status === "api_key_only" ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
|
||||||
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 shrink-0" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-blue-400">
|
|
||||||
API Key Detected - CLI Not Installed
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-blue-400/80 mt-1">
|
|
||||||
{status.recommendation ||
|
|
||||||
"OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{status.installCommands && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium text-foreground-secondary">
|
|
||||||
Installation Commands:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{status.installCommands.npm && (
|
|
||||||
<div className="p-2 rounded bg-background border border-border-glass">
|
|
||||||
<p className="text-xs text-muted-foreground mb-1">npm:</p>
|
|
||||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
|
||||||
{status.installCommands.npm}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
|
||||||
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-yellow-400">
|
|
||||||
Codex CLI Not Detected
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-yellow-400/80 mt-1">
|
|
||||||
{status.recommendation ||
|
|
||||||
"Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{status.installCommands && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium text-foreground-secondary">
|
|
||||||
Installation Commands:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{status.installCommands.npm && (
|
|
||||||
<div className="p-2 rounded bg-background border border-border-glass">
|
|
||||||
<p className="text-xs text-muted-foreground mb-1">npm:</p>
|
|
||||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
|
||||||
{status.installCommands.npm}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{status.installCommands.macos && (
|
|
||||||
<div className="p-2 rounded bg-background border border-border-glass">
|
|
||||||
<p className="text-xs text-muted-foreground mb-1">
|
|
||||||
macOS (Homebrew):
|
|
||||||
</p>
|
|
||||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
|
||||||
{status.installCommands.macos}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Settings } from "lucide-react";
|
|
||||||
|
|
||||||
interface SettingsHeaderProps {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsHeader({
|
|
||||||
title = "Settings",
|
|
||||||
description = "Configure your API keys and preferences",
|
|
||||||
}: SettingsHeaderProps) {
|
|
||||||
return (
|
|
||||||
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
|
||||||
<div className="px-8 py-6">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
|
||||||
<Settings className="w-5 h-5 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { Project } from "@/lib/electron";
|
|
||||||
import type { NavigationItem } from "../config/navigation";
|
|
||||||
|
|
||||||
interface SettingsNavigationProps {
|
|
||||||
navItems: NavigationItem[];
|
|
||||||
activeSection: string;
|
|
||||||
currentProject: Project | null;
|
|
||||||
onNavigate: (sectionId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsNavigation({
|
|
||||||
navItems,
|
|
||||||
activeSection,
|
|
||||||
currentProject,
|
|
||||||
onNavigate,
|
|
||||||
}: SettingsNavigationProps) {
|
|
||||||
return (
|
|
||||||
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm">
|
|
||||||
<div className="sticky top-0 p-4 space-y-1">
|
|
||||||
{navItems
|
|
||||||
.filter((item) => item.id !== "danger" || currentProject)
|
|
||||||
.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
const isActive = activeSection === item.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => onNavigate(item.id)}
|
|
||||||
className={cn(
|
|
||||||
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left",
|
|
||||||
isActive
|
|
||||||
? "bg-brand-500/10 text-brand-500 border border-brand-500/20"
|
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={cn(
|
|
||||||
"w-4 h-4 shrink-0",
|
|
||||||
isActive ? "text-brand-500" : ""
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="truncate">{item.label}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user