mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-15 21:34:36 +00:00
Compare commits
556 Commits
thinking-t
...
github-v1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21bee38610 | ||
|
|
da6e0e60c0 | ||
|
|
d89b567b47 | ||
|
|
34eb03f5b8 | ||
|
|
2f6d15a51e | ||
|
|
8ffea80980 | ||
|
|
c87d61b561 | ||
|
|
35c12e2053 | ||
|
|
33d8bfc937 | ||
|
|
f2343a6794 | ||
|
|
bab000eeb5 | ||
|
|
8e674ae053 | ||
|
|
6a4f4009d5 | ||
|
|
5e79b95927 | ||
|
|
a7a2bbb497 | ||
|
|
6e93d14bdb | ||
|
|
f29f284b3e | ||
|
|
b1b8f6cf71 | ||
|
|
4c3336bbe7 | ||
|
|
354ac0b493 | ||
|
|
1d159c6858 | ||
|
|
d70639b256 | ||
|
|
e4a92f0084 | ||
|
|
fdf5a70a27 | ||
|
|
f71da42520 | ||
|
|
f6bdeb9e3a | ||
|
|
2400354bab | ||
|
|
db348c46cc | ||
|
|
49567fe61a | ||
|
|
e5b3f796e4 | ||
|
|
a9700c8773 | ||
|
|
26cf5e003e | ||
|
|
742cf10dee | ||
|
|
7664453f94 | ||
|
|
460672aa93 | ||
|
|
b4e4fd9807 | ||
|
|
34bdfd0937 | ||
|
|
84591ca8ad | ||
|
|
fd4d0c5c0b | ||
|
|
9f5db46911 | ||
|
|
755ddbb223 | ||
|
|
701d470d01 | ||
|
|
1d9058d26b | ||
|
|
39e2a5f595 | ||
|
|
f862ab6722 | ||
|
|
129d4f0b1b | ||
|
|
3a1e50d1f8 | ||
|
|
e2fb690d8e | ||
|
|
0a7f58a811 | ||
|
|
dae0168ed8 | ||
|
|
edfe2e4f1c | ||
|
|
1bc1ea8b47 | ||
|
|
dacbbe3184 | ||
|
|
89285d8f5f | ||
|
|
2e853911c3 | ||
|
|
695fdecf23 | ||
|
|
054d22791d | ||
|
|
4a57cc69d8 | ||
|
|
7e0c8db029 | ||
|
|
ba4cc3bf86 | ||
|
|
b19a424c85 | ||
|
|
1689281c35 | ||
|
|
cdbb59fae8 | ||
|
|
4eb311e98f | ||
|
|
80eac96258 | ||
|
|
4bad6f9f1b | ||
|
|
d7db57e8e1 | ||
|
|
943fbf39a3 | ||
|
|
d8a34c2fcc | ||
|
|
5720ed1f44 | ||
|
|
bb20a359e4 | ||
|
|
0d472a49a0 | ||
|
|
203581e82f | ||
|
|
677631916c | ||
|
|
1aa1e8c904 | ||
|
|
55d62fbd9f | ||
|
|
e1ad2a355c | ||
|
|
4f318f913e | ||
|
|
2d814b6db2 | ||
|
|
e561f1ad68 | ||
|
|
ebfb985215 | ||
|
|
2646da50df | ||
|
|
50a5f6e53b | ||
|
|
d03fac52e7 | ||
|
|
6a802c01cd | ||
|
|
14146428dd | ||
|
|
26d0280f70 | ||
|
|
3274a5813e | ||
|
|
382905602c | ||
|
|
8b5cea7899 | ||
|
|
100c31cbb1 | ||
|
|
0b286f1b84 | ||
|
|
2f6ca958fe | ||
|
|
5218e7a546 | ||
|
|
7f8e799392 | ||
|
|
289f4abaaa | ||
|
|
7ce898ce43 | ||
|
|
0dd716a75e | ||
|
|
87171467fa | ||
|
|
b99afdad91 | ||
|
|
4fd576f3af | ||
|
|
2f41d0bedd | ||
|
|
5f03290534 | ||
|
|
427157c683 | ||
|
|
a0ab3d98b7 | ||
|
|
c8de766913 | ||
|
|
d57b963141 | ||
|
|
0ebcaff927 | ||
|
|
15931fa170 | ||
|
|
af4087d7b5 | ||
|
|
323ea1040c | ||
|
|
1fe87b0233 | ||
|
|
8d11df1b3b | ||
|
|
ecc5050838 | ||
|
|
606cf3b6f2 | ||
|
|
67cfd7f06b | ||
|
|
ab9ac7c87a | ||
|
|
ee9f979613 | ||
|
|
228b6444f8 | ||
|
|
9998efdae2 | ||
|
|
9427f56e1a | ||
|
|
a6dd35d73d | ||
|
|
faeaafa5f5 | ||
|
|
8b298a233e | ||
|
|
6f43d03043 | ||
|
|
c868a4088d | ||
|
|
83d8a88c90 | ||
|
|
268f37f8c9 | ||
|
|
b0aaf04957 | ||
|
|
b7875256f3 | ||
|
|
7bc47fb904 | ||
|
|
5cf8e54372 | ||
|
|
7437ccd6f4 | ||
|
|
4bf882ba81 | ||
|
|
d5dcc55a47 | ||
|
|
e1925f4fe8 | ||
|
|
ee3d034e16 | ||
|
|
257a4d5b86 | ||
|
|
1fc5836f64 | ||
|
|
2fb89161c8 | ||
|
|
251fbc0a99 | ||
|
|
0da901a188 | ||
|
|
17221e6ffe | ||
|
|
cc9f88ac8f | ||
|
|
fe65ed6a61 | ||
|
|
e37a75a411 | ||
|
|
194ff4919c | ||
|
|
83843a794f | ||
|
|
235a60d3c2 | ||
|
|
b70d186bd1 | ||
|
|
647331de28 | ||
|
|
57ef115375 | ||
|
|
942498211f | ||
|
|
e789fcf5e5 | ||
|
|
b9fb180bc6 | ||
|
|
7427b887f9 | ||
|
|
289b2b6a51 | ||
|
|
49b4b5907e | ||
|
|
f82442c123 | ||
|
|
e682cc9daf | ||
|
|
d359e086a4 | ||
|
|
f949755367 | ||
|
|
a168d854f4 | ||
|
|
31645f5578 | ||
|
|
a1b68daa9a | ||
|
|
ca65da2d9e | ||
|
|
e48d804d84 | ||
|
|
b4209582fb | ||
|
|
dbdea2f659 | ||
|
|
a50ab4b5b5 | ||
|
|
4d7c3f56fa | ||
|
|
16b41d2bea | ||
|
|
a8c499ae8f | ||
|
|
24430287c5 | ||
|
|
1f52731255 | ||
|
|
4a3ba58f65 | ||
|
|
2a3a8a1ec2 | ||
|
|
69e562125d | ||
|
|
b5e97eb338 | ||
|
|
16e6941495 | ||
|
|
f033e0317e | ||
|
|
ddd88f92cc | ||
|
|
99101edc13 | ||
|
|
6e85a07977 | ||
|
|
be1a3536ae | ||
|
|
1e4bfbcf6f | ||
|
|
204e3bf382 | ||
|
|
8fb014a48d | ||
|
|
57c3cf1f8b | ||
|
|
f9d0850c5e | ||
|
|
8864da7a77 | ||
|
|
1b39199083 | ||
|
|
b8204c0bb7 | ||
|
|
fe8c5c143e | ||
|
|
d6f86e9bb7 | ||
|
|
bf00b2bfc9 | ||
|
|
382ec8fb2c | ||
|
|
6454adcd69 | ||
|
|
99548554d7 | ||
|
|
751899eeec | ||
|
|
f8df1d3185 | ||
|
|
b07a47fc89 | ||
|
|
c6f84f32d7 | ||
|
|
ebe25c3e9a | ||
|
|
65d7fc3ccd | ||
|
|
4f3037d803 | ||
|
|
5c490c51ed | ||
|
|
5da1c0087b | ||
|
|
4375149e63 | ||
|
|
b695d3b6bb | ||
|
|
d7e133732c | ||
|
|
494e6fff01 | ||
|
|
0c7a297b1d | ||
|
|
9b1f9007c3 | ||
|
|
34ef5f4ece | ||
|
|
73ad20b90c | ||
|
|
340e80257a | ||
|
|
c23ea2a211 | ||
|
|
a5f964aec6 | ||
|
|
b8a8fb0de6 | ||
|
|
a6a8f41fd3 | ||
|
|
c137babea3 | ||
|
|
db2abc1b2c | ||
|
|
a0f9f8dabb | ||
|
|
8a185aa678 | ||
|
|
29aaf4f000 | ||
|
|
fc940dfcfb | ||
|
|
2f2ea98937 | ||
|
|
ef0fa2007b | ||
|
|
f07d4b933c | ||
|
|
5f57cee8e4 | ||
|
|
1755a3fe07 | ||
|
|
99680baf83 | ||
|
|
9aa5460a0e | ||
|
|
b4014e5baa | ||
|
|
96e4dcb521 | ||
|
|
7e682a95c4 | ||
|
|
5eeba76bc5 | ||
|
|
a2c91ebc32 | ||
|
|
1aee8b49e1 | ||
|
|
984f17ddd7 | ||
|
|
d556143e3b | ||
|
|
7e3ad770ac | ||
|
|
87524de265 | ||
|
|
ee10d9b898 | ||
|
|
bbd36e8441 | ||
|
|
4e2d1acf7d | ||
|
|
40d63cd1e3 | ||
|
|
77b2331428 | ||
|
|
2b7e2edee5 | ||
|
|
28aba35ff9 | ||
|
|
89219a77f7 | ||
|
|
20e3a74bad | ||
|
|
ff690350b1 | ||
|
|
ebefb26e8f | ||
|
|
0b1ee9ddd9 | ||
|
|
79599f351e | ||
|
|
8c9f6b1d3e | ||
|
|
83bcb9e95b | ||
|
|
96b9ff8d0e | ||
|
|
0af2254856 | ||
|
|
c2944024a8 | ||
|
|
5be4bda90f | ||
|
|
b78e2db013 | ||
|
|
3f4d1121a4 | ||
|
|
def910021d | ||
|
|
3ac42e9632 | ||
|
|
9c26bb7c6c | ||
|
|
53f20f7612 | ||
|
|
11b3927dc2 | ||
|
|
a190eda2c8 | ||
|
|
1f18f389c0 | ||
|
|
84e56ee614 | ||
|
|
59329a414d | ||
|
|
452c991f58 | ||
|
|
be8116e2ea | ||
|
|
f0ed1e38c9 | ||
|
|
ac0f1dbbdd | ||
|
|
275a352e81 | ||
|
|
9f3bc0e352 | ||
|
|
6c1a1a77b7 | ||
|
|
2e21c62320 | ||
|
|
19c6fec4d1 | ||
|
|
4779d99a13 | ||
|
|
05e0759878 | ||
|
|
2330ec6dc3 | ||
|
|
75e5130cf8 | ||
|
|
87efd27459 | ||
|
|
62f080b0e4 | ||
|
|
ae3990a557 | ||
|
|
d7b5b431d6 | ||
|
|
e2fbd098d2 | ||
|
|
ef78fd8bae | ||
|
|
72ebaeb8f7 | ||
|
|
0dc62d5dad | ||
|
|
d118782a10 | ||
|
|
ff05647350 | ||
|
|
0e1c711c4e | ||
|
|
bfb254dac6 | ||
|
|
92fe927785 | ||
|
|
2e25fe9d5d | ||
|
|
38c5f23f4a | ||
|
|
112c58abf5 | ||
|
|
0dce5173cc | ||
|
|
2c70c0b00f | ||
|
|
34024c2504 | ||
|
|
27e826eba6 | ||
|
|
89a4f1c1ae | ||
|
|
c0c61b25ff | ||
|
|
0d1c6e0ca9 | ||
|
|
002db3abf4 | ||
|
|
416a919c6d | ||
|
|
dbbcf0b8d0 | ||
|
|
efac8cebb3 | ||
|
|
4f2baf1a72 | ||
|
|
48b2bde6e5 | ||
|
|
88314148e6 | ||
|
|
56452d886d | ||
|
|
f3e64cfb19 | ||
|
|
8fcc80bc20 | ||
|
|
0beccc406e | ||
|
|
b82ea693db | ||
|
|
4fd9a19fbb | ||
|
|
e16487b804 | ||
|
|
5388192aac | ||
|
|
8010448ba1 | ||
|
|
66f3e69867 | ||
|
|
ca599ab8fc | ||
|
|
c3b3b133b0 | ||
|
|
300ec0e0af | ||
|
|
6632987827 | ||
|
|
e555e893c4 | ||
|
|
81134cf61e | ||
|
|
37e4c1e619 | ||
|
|
02b5e7d72c | ||
|
|
7abc2a947e | ||
|
|
337a7e9646 | ||
|
|
62cc532ecc | ||
|
|
d5a506d4ae | ||
|
|
9c5f94bd66 | ||
|
|
83390314d6 | ||
|
|
8b08e9cda5 | ||
|
|
b1b1df824c | ||
|
|
7d1733c752 | ||
|
|
cf05e6e02b | ||
|
|
7e49d0fb15 | ||
|
|
c4f63824df | ||
|
|
4236744fb5 | ||
|
|
284c045795 | ||
|
|
2c53abd70c | ||
|
|
b7a9cbfc68 | ||
|
|
46a35dfc1b | ||
|
|
b7597c12dd | ||
|
|
6830590183 | ||
|
|
b9b4349039 | ||
|
|
4107918909 | ||
|
|
6347ee9988 | ||
|
|
9daa4e04ea | ||
|
|
ed96ae9d45 | ||
|
|
8ce0966987 | ||
|
|
8cb26b6066 | ||
|
|
5cf6a1343c | ||
|
|
44d6c5780d | ||
|
|
5eaa8e1bf4 | ||
|
|
df2713a6c2 | ||
|
|
ff6864a7ca | ||
|
|
5e37a902ce | ||
|
|
df2ebfac7d | ||
|
|
5fbcb203f5 | ||
|
|
34db739442 | ||
|
|
ae8c4154aa | ||
|
|
315836c0b7 | ||
|
|
c0d009d5f3 | ||
|
|
c36f3b9dbe | ||
|
|
d31824320e | ||
|
|
88c0675148 | ||
|
|
82c4755fb0 | ||
|
|
40572eeba4 | ||
|
|
d81d63045a | ||
|
|
ece3bfd93d | ||
|
|
acd91bddf7 | ||
|
|
3a14ca044c | ||
|
|
d66d806700 | ||
|
|
e9b95b2e91 | ||
|
|
56dde2cc83 | ||
|
|
d2ce368a3f | ||
|
|
f492122d59 | ||
|
|
b0f77da56c | ||
|
|
274b86b19b | ||
|
|
2ca118db59 | ||
|
|
a0c0e2b5c3 | ||
|
|
d43fbec12d | ||
|
|
bb426112ed | ||
|
|
d2217bb825 | ||
|
|
ac495bd351 | ||
|
|
b913eb7acc | ||
|
|
ea65a91b2e | ||
|
|
ed6d749104 | ||
|
|
9eefcd1b41 | ||
|
|
7c1124199e | ||
|
|
5cf126d489 | ||
|
|
509f7d9617 | ||
|
|
ae1bf92c81 | ||
|
|
b021b26e77 | ||
|
|
9555d348de | ||
|
|
220c564047 | ||
|
|
cf5c0129ac | ||
|
|
543dbe71d2 | ||
|
|
54569b5552 | ||
|
|
6a09861806 | ||
|
|
79a4c65313 | ||
|
|
654534ac71 | ||
|
|
ba16bfdf3d | ||
|
|
ad5614bbb9 | ||
|
|
dda579c8ad | ||
|
|
4246cdb069 | ||
|
|
7ade6d386d | ||
|
|
2613f44961 | ||
|
|
62ffeb3987 | ||
|
|
4a8e8f537c | ||
|
|
a68bee7878 | ||
|
|
ed33d82535 | ||
|
|
2d63c22d1a | ||
|
|
e22af25076 | ||
|
|
622caae9c9 | ||
|
|
fed4776451 | ||
|
|
fdf560c343 | ||
|
|
fc3ffb2bf9 | ||
|
|
7368342bab | ||
|
|
c8fc910533 | ||
|
|
0f9ef84d55 | ||
|
|
74b5c285cf | ||
|
|
a34e67b518 | ||
|
|
0c7f0cfa2e | ||
|
|
10ee6d345b | ||
|
|
48ec68730f | ||
|
|
70e4efe429 | ||
|
|
92948ed8a4 | ||
|
|
6d412d8872 | ||
|
|
e6a0a005d6 | ||
|
|
90d44751e7 | ||
|
|
4d062ba1b2 | ||
|
|
f8bca50f00 | ||
|
|
3d2ef28fa8 | ||
|
|
210b3e905b | ||
|
|
96975ef8d6 | ||
|
|
b8b998be56 | ||
|
|
d8ac35f6e5 | ||
|
|
ed1eacfce0 | ||
|
|
629f475f63 | ||
|
|
43a7c1dd8c | ||
|
|
e288ce0fca | ||
|
|
67b3fcb31a | ||
|
|
aedb5550a8 | ||
|
|
1638ffde69 | ||
|
|
d4cfbd8219 | ||
|
|
c7bac83212 | ||
|
|
fc9789d7a7 | ||
|
|
a8957d8d16 | ||
|
|
0660433921 | ||
|
|
1a6f4f1c0d | ||
|
|
974a24ba02 | ||
|
|
5ebe29de1e | ||
|
|
6bdf8b1fe1 | ||
|
|
5bcc93851c | ||
|
|
d0789632b4 | ||
|
|
a6e297baad | ||
|
|
307af10c8b | ||
|
|
f254cf76d9 | ||
|
|
b4ffaa21ec | ||
|
|
7bf6f264e4 | ||
|
|
7434fbba8e | ||
|
|
b7581e01ea | ||
|
|
b46d4789fc | ||
|
|
199bd8a9a2 | ||
|
|
decf2452c4 | ||
|
|
d8663a44c2 | ||
|
|
8917a4c609 | ||
|
|
5d7a52f8b8 | ||
|
|
b7b827c5bd | ||
|
|
613e082358 | ||
|
|
b6856bd593 | ||
|
|
7cb5a77ba6 | ||
|
|
cd9898a565 | ||
|
|
a4ffa869cc | ||
|
|
dbc84ff4c3 | ||
|
|
c11ea3fd92 | ||
|
|
3c3a0f8afb | ||
|
|
b93614cb81 | ||
|
|
b84d513bd7 | ||
|
|
0554d03162 | ||
|
|
15caecdb45 | ||
|
|
91ab966921 | ||
|
|
bc3286de46 | ||
|
|
af45444496 | ||
|
|
43202f2820 | ||
|
|
ce37e11bfe | ||
|
|
6e9833acce | ||
|
|
379c4ecab3 | ||
|
|
f1db4b60c4 | ||
|
|
9846b26be7 | ||
|
|
d6ba6af6f3 | ||
|
|
d463ade028 | ||
|
|
6c3495a75a | ||
|
|
a16edb4ea0 | ||
|
|
9efe09564b | ||
|
|
ccdd77032a | ||
|
|
41e234c6d0 | ||
|
|
3e03646e42 | ||
|
|
f7acc34327 | ||
|
|
bf420e7df6 | ||
|
|
78484f545c | ||
|
|
ad008d2151 | ||
|
|
651a10d6db | ||
|
|
f9674793af | ||
|
|
f3a33d41f1 | ||
|
|
642eec3dfd | ||
|
|
73513612d4 | ||
|
|
9b77246246 | ||
|
|
cf3bc1e0a6 | ||
|
|
4550ad049e | ||
|
|
d51c6ca39f | ||
|
|
47c6a2430c | ||
|
|
909013320b | ||
|
|
770a4d87db | ||
|
|
2e417c4d8c | ||
|
|
2da527aaa6 | ||
|
|
0303eb0cc1 | ||
|
|
3f4a792c8a | ||
|
|
9629f7464b | ||
|
|
9017d10303 | ||
|
|
59d4041aa4 | ||
|
|
feb8c4f3c6 | ||
|
|
3f5cd2c4a8 | ||
|
|
a160eb76df | ||
|
|
e4b2028f91 | ||
|
|
ffc48e115b | ||
|
|
04b4dacee3 | ||
|
|
c0e30f48c6 | ||
|
|
99158e736b | ||
|
|
4c02d515a1 | ||
|
|
b803a9732d | ||
|
|
f9d5e18790 | ||
|
|
147111c9c6 | ||
|
|
9a70eb538b | ||
|
|
0b1731142e | ||
|
|
7ec48dfd15 | ||
|
|
57120e69ed | ||
|
|
11efda3f5c | ||
|
|
a5cb4e41f5 | ||
|
|
88b2382b97 | ||
|
|
237c0253c2 | ||
|
|
a9f27371cf | ||
|
|
9c126c5b64 | ||
|
|
e241aa21b9 | ||
|
|
1fd005838c |
69
.github/workflows/docs-update.yml
vendored
69
.github/workflows/docs-update.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: Docs Update
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
if: github.repository == 'sst/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch full history to access commits
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Get recent commits
|
||||
id: commits
|
||||
run: |
|
||||
COMMITS=$(git log --since="4 hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "No commits in the last 4 hours"
|
||||
echo "has_commits=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_commits=true" >> $GITHUB_OUTPUT
|
||||
{
|
||||
echo "list<<EOF"
|
||||
echo "$COMMITS"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run opencode
|
||||
if: steps.commits.outputs.has_commits == 'true'
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/gpt-5.2
|
||||
agent: docs
|
||||
prompt: |
|
||||
Review the following commits from the last 4 hours and identify any new features that may need documentation.
|
||||
|
||||
<recent_commits>
|
||||
${{ steps.commits.outputs.list }}
|
||||
</recent_commits>
|
||||
|
||||
Steps:
|
||||
1. For each commit that looks like a new feature or significant change:
|
||||
- Read the changed files to understand what was added
|
||||
- Check if the feature is already documented in packages/web/src/content/docs/*
|
||||
2. If you find undocumented features:
|
||||
- Update the relevant documentation files in packages/web/src/content/docs/*
|
||||
- Follow the existing documentation style and structure
|
||||
- Make sure to document the feature clearly with examples where appropriate
|
||||
3. If all new features are already documented, report that no updates are needed
|
||||
4. If you are creating a new documentation file be sure to update packages/web/astro.config.mjs too.
|
||||
|
||||
Focus on user-facing features and API changes. Skip internal refactors, bug fixes, and test updates unless they affect user-facing behavior.
|
||||
Don't feel the need to document every little thing. It is perfectly okay to make 0 changes at all.
|
||||
Try to keep documentation only for large features or changes that already have a good spot to be documented.
|
||||
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -79,12 +79,6 @@ jobs:
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: false
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
outputs:
|
||||
release: ${{ steps.publish.outputs.release }}
|
||||
tag: ${{ steps.publish.outputs.tag }}
|
||||
@@ -105,8 +99,6 @@ jobs:
|
||||
target: x86_64-pc-windows-msvc
|
||||
- host: blacksmith-4vcpu-ubuntu-2404
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -151,12 +143,13 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/desktop/src-tauri
|
||||
workspaces: packages/tauri/src-tauri
|
||||
shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Prepare
|
||||
if: inputs.bump || inputs.version
|
||||
run: |
|
||||
cd packages/desktop
|
||||
cd packages/tauri
|
||||
bun ./scripts/prepare.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
@@ -166,7 +159,6 @@ jobs:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- name: Install tauri-cli from portable appimage branch
|
||||
@@ -191,10 +183,10 @@ jobs:
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
with:
|
||||
projectPath: packages/desktop
|
||||
projectPath: packages/tauri
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
|
||||
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.release }}
|
||||
tagName: ${{ needs.publish.outputs.tag }}
|
||||
|
||||
3
.github/workflows/stats.yml
vendored
3
.github/workflows/stats.yml
vendored
@@ -5,11 +5,8 @@ on:
|
||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
if: github.repository == 'sst/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
6
.github/workflows/sync-zed-extension.yml
vendored
6
.github/workflows/sync-zed-extension.yml
vendored
@@ -2,8 +2,8 @@ name: "sync-zed-extension"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
# release:
|
||||
# types: [published]
|
||||
|
||||
jobs:
|
||||
zed:
|
||||
@@ -31,4 +31,4 @@ jobs:
|
||||
run: |
|
||||
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
|
||||
@@ -6,8 +6,6 @@ You are an expert technical documentation writer
|
||||
|
||||
You are not verbose
|
||||
|
||||
Use a relaxed and friendly tone
|
||||
|
||||
The title of the page should be a word or a 2-3 word phrase
|
||||
|
||||
The description should be one short line, should not start with "The", should
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: test-skill
|
||||
description: use this when asked to test skill
|
||||
---
|
||||
|
||||
woah this is a test skill
|
||||
29
AGENTS.md
29
AGENTS.md
@@ -4,4 +4,31 @@
|
||||
|
||||
## Tool Calling
|
||||
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
|
||||
|
||||
json
|
||||
{
|
||||
"recipient_name": "multi_tool_use.parallel",
|
||||
"parameters": {
|
||||
"tool_uses": [
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.tsx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.md"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@ your debugger via that URL. Other methods can result in breakpoints being mapped
|
||||
|
||||
Caveats:
|
||||
|
||||
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
|
||||
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
|
||||
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
|
||||
is triggered.
|
||||
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
|
||||
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
|
||||
- If `spawn` does not work for you, you can debug the server separately:
|
||||
- Debug server: `bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096`,
|
||||
then attach TUI with `opencode attach http://localhost:4096`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts`
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ you can switch between these using the `Tab` key.
|
||||
- Asks permission before running bash commands
|
||||
- Ideal for exploring unfamiliar codebases or planning changes
|
||||
|
||||
Also, included is a **general** subagent for complex searches and multistep tasks.
|
||||
Also, included is a **general** subagent for complex searches and multi-step tasks.
|
||||
This is used internally and can be invoked using `@general` in messages.
|
||||
|
||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||
@@ -98,7 +98,7 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
||||
|
||||
### FAQ
|
||||
|
||||
#### How is this different from Claude Code?
|
||||
#### How is this different than Claude Code?
|
||||
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
|
||||
115
README.zh-TW.md
115
README.zh-TW.md
@@ -1,115 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai">
|
||||
<picture>
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">開源的 AI Coding Agent。</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
### 安裝
|
||||
|
||||
```bash
|
||||
# 直接安裝 (YOLO)
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# 套件管理員
|
||||
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS 與 Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g github:sst/opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 安裝前請先移除 0.1.x 以前的舊版本。
|
||||
|
||||
### 桌面應用程式 (BETA)
|
||||
|
||||
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
||||
|
||||
| 平台 | 下載連結 |
|
||||
| --------------------- | ------------------------------------- |
|
||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||
| Linux | `.deb`, `.rpm`, 或 AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew Cask)
|
||||
brew install --cask opencode-desktop
|
||||
```
|
||||
|
||||
#### 安裝目錄
|
||||
|
||||
安裝腳本會依據以下優先順序決定安裝路徑:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄
|
||||
2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑
|
||||
3. `$HOME/bin` - 標準使用者執行檔目錄 (若存在或可建立)
|
||||
4. `$HOME/.opencode/bin` - 預設備用路徑
|
||||
|
||||
```bash
|
||||
# 範例
|
||||
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### Agents
|
||||
|
||||
OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
||||
|
||||
- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。
|
||||
- **plan** - 唯讀模式,適用於程式碼分析與探索。
|
||||
- 預設禁止修改檔案。
|
||||
- 執行 bash 指令前會詢問權限。
|
||||
- 非常適合用來探索陌生的程式碼庫或規劃變更。
|
||||
|
||||
此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。
|
||||
|
||||
了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。
|
||||
|
||||
### 線上文件
|
||||
|
||||
關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。
|
||||
|
||||
### 參與貢獻
|
||||
|
||||
如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。
|
||||
|
||||
### 基於 OpenCode 進行開發
|
||||
|
||||
如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。
|
||||
|
||||
### 常見問題 (FAQ)
|
||||
|
||||
#### 這跟 Claude Code 有什麼不同?
|
||||
|
||||
在功能面上與 Claude Code 非常相似。以下是關鍵差異:
|
||||
|
||||
- 100% 開源。
|
||||
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
|
||||
- 內建 LSP (語言伺服器協定) 支援。
|
||||
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
|
||||
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
|
||||
|
||||
#### 另一個同名的 Repo 是什麼?
|
||||
|
||||
另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。
|
||||
|
||||
---
|
||||
|
||||
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
6
STATS.md
6
STATS.md
@@ -176,9 +176,3 @@
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766870016,
|
||||
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
|
||||
"lastModified": 1766025857,
|
||||
"narHash": "sha256-Lav5jJazCW4mdg1iHcROpuXqmM94BWJvabLFWaJVJp0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
|
||||
"rev": "def3da69945bbe338c373fddad5a1bb49cf199ce",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -574,13 +574,10 @@ async function subscribeSessionEvents() {
|
||||
}
|
||||
|
||||
async function summarize(response: string) {
|
||||
const payload = useContext().payload as IssueCommentEvent
|
||||
try {
|
||||
return await chat(`Summarize the following in less than 40 characters:\n\n${response}`)
|
||||
} catch (e) {
|
||||
if (isScheduleEvent()) {
|
||||
return "Scheduled task changes"
|
||||
}
|
||||
const payload = useContext().payload as IssueCommentEvent
|
||||
return `Fix issue: ${payload.issue.title}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +44,3 @@ new sst.cloudflare.x.Astro("Web", {
|
||||
VITE_API_URL: api.url.apply((url) => url!),
|
||||
},
|
||||
})
|
||||
|
||||
new sst.cloudflare.StaticSite("WebApp", {
|
||||
domain: "app." + domain,
|
||||
path: "packages/app",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -103,7 +103,6 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
@@ -119,7 +118,6 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
////////////////
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
@@ -138,7 +136,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
bucket,
|
||||
bucketNew,
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
|
||||
10
infra/desktop.ts
Normal file
10
infra/desktop.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { domain } from "./stage"
|
||||
|
||||
new sst.cloudflare.StaticSite("Desktop", {
|
||||
domain: "desktop." + domain,
|
||||
path: "packages/desktop",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
124
install
124
install
@@ -7,51 +7,7 @@ RED='\033[0;31m'
|
||||
ORANGE='\033[38;5;214m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
OpenCode Installer
|
||||
|
||||
Usage: install.sh [options]
|
||||
|
||||
Options:
|
||||
-h, --help Display this help message
|
||||
-v, --version <version> Install a specific version (e.g., 1.0.180)
|
||||
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
|
||||
|
||||
Examples:
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
|
||||
EOF
|
||||
}
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
no_modify_path=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-v|--version)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
requested_version="$2"
|
||||
shift 2
|
||||
else
|
||||
echo -e "${RED}Error: --version requires a version argument${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--no-modify-path)
|
||||
no_modify_path=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
@@ -155,18 +111,8 @@ if [ -z "$requested_version" ]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
|
||||
# Verify the release exists before downloading
|
||||
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}")
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
|
||||
echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
print_message() {
|
||||
@@ -358,42 +304,42 @@ case $current_shell in
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$no_modify_path" != "true" ]]; then
|
||||
config_file=""
|
||||
for file in $config_files; do
|
||||
if [[ -f $file ]]; then
|
||||
config_file=$file
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $config_file ]]; then
|
||||
print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
case $current_shell in
|
||||
fish)
|
||||
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
zsh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
bash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
ash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
sh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
esac
|
||||
config_file=""
|
||||
for file in $config_files; do
|
||||
if [[ -f $file ]]; then
|
||||
config_file=$file
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $config_file ]]; then
|
||||
print_message error "No config file found for $current_shell. Checked files: ${config_files[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
case $current_shell in
|
||||
fish)
|
||||
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
zsh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
bash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
ash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
sh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-d10pu1tpwBbZ4YZqG0WcaK6W1+K2+Q0wdPjuI1rVIpY="
|
||||
"nodeModules": "sha256-cpXmqJQJeFj3eED/aOb4YLUdkZFV//7u4f0STBxzUhk="
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.0.2",
|
||||
"@pierre/diffs": "1.0.0-beta.3",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
@@ -40,13 +40,10 @@
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "17.0.1",
|
||||
"marked-shiki": "1.2.1",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"shiki": "3.20.0",
|
||||
"solid-list": "0.3.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"virtua": "0.42.3",
|
||||
@@ -59,7 +56,6 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
@@ -68,7 +64,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
@@ -85,6 +80,7 @@
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"sharp",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"web-tree-sitter"
|
||||
|
||||
1
packages/app/.gitignore
vendored
1
packages/app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
src/assets/theme.css
|
||||
@@ -1,34 +0,0 @@
|
||||
## Usage
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.203",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./vite": "./vite.js"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "20.0.11",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||
"vite-plugin-solid": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solid-primitives/websocket": "1.3.1",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.3.0",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
"remeda": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"virtua": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/assets/*.js
|
||||
Content-Type: application/javascript
|
||||
|
||||
/assets/*.mjs
|
||||
Content-Type: application/javascript
|
||||
|
||||
/assets/*.css
|
||||
Content-Type: text/css
|
||||
|
||||
/*.js
|
||||
Content-Type: application/javascript
|
||||
|
||||
/*.mjs
|
||||
Content-Type: application/javascript
|
||||
|
||||
/*.css
|
||||
Content-Type: text/css
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Component, createMemo, createSignal, Show } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
|
||||
export const DialogSelectMcp: Component = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const [loading, setLoading] = createSignal<string | null>(null)
|
||||
|
||||
const items = createMemo(() =>
|
||||
Object.entries(sync.data.mcp ?? {})
|
||||
.map(([name, status]) => ({ name, status: status.status }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
)
|
||||
|
||||
const toggle = async (name: string) => {
|
||||
if (loading()) return
|
||||
setLoading(name)
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
} else {
|
||||
await sdk.client.mcp.connect({ name })
|
||||
}
|
||||
const result = await sdk.client.mcp.status()
|
||||
if (result.data) sync.set("mcp", result.data)
|
||||
setLoading(null)
|
||||
}
|
||||
|
||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||
const totalCount = createMemo(() => items().length)
|
||||
|
||||
return (
|
||||
<Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
|
||||
<List
|
||||
search={{ placeholder: "Search", autofocus: true }}
|
||||
emptyMessage="No MCPs configured"
|
||||
key={(x) => x?.name ?? ""}
|
||||
items={items}
|
||||
filterKeys={["name", "status"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
onSelect={(x) => {
|
||||
if (x) toggle(x.name)
|
||||
}}
|
||||
>
|
||||
{(i) => {
|
||||
const mcpStatus = () => sync.data.mcp[i.name]
|
||||
const status = () => mcpStatus()?.status
|
||||
const error = () => {
|
||||
const s = mcpStatus()
|
||||
return s?.status === "failed" ? s.error : undefined
|
||||
}
|
||||
const enabled = () => status() === "connected"
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between gap-x-3">
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={status() === "connected"}>
|
||||
<span class="text-11-regular text-text-weaker">connected</span>
|
||||
</Show>
|
||||
<Show when={status() === "failed"}>
|
||||
<span class="text-11-regular text-text-weaker">failed</span>
|
||||
</Show>
|
||||
<Show when={status() === "needs_auth"}>
|
||||
<span class="text-11-regular text-text-weaker">needs auth</span>
|
||||
</Show>
|
||||
<Show when={status() === "disabled"}>
|
||||
<span class="text-11-regular text-text-weaker">disabled</span>
|
||||
</Show>
|
||||
<Show when={loading() === i.name}>
|
||||
<span class="text-11-regular text-text-weak">...</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={error()}>
|
||||
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export function SessionContextUsage() {
|
||||
const sync = useSync()
|
||||
const params = useParams()
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
|
||||
const cost = createMemo(() => {
|
||||
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(total)
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
|
||||
if (!last) return
|
||||
const total =
|
||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||
const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||
return {
|
||||
tokens: total.toLocaleString(),
|
||||
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={context?.()}>
|
||||
{(ctx) => (
|
||||
<Tooltip
|
||||
value={
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Tokens</span>
|
||||
<span class="text-left flex-1">{ctx().tokens}</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Usage</span>
|
||||
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Cost</span>
|
||||
<span class="text-left flex-1">{cost()}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
|
||||
{/* <span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span> */}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
|
||||
export function SessionLspIndicator() {
|
||||
const sync = useSync()
|
||||
|
||||
const lspStats = createMemo(() => {
|
||||
const lsp = sync.data.lsp ?? []
|
||||
const connected = lsp.filter((s) => s.status === "connected").length
|
||||
const hasError = lsp.some((s) => s.status === "error")
|
||||
const total = lsp.length
|
||||
return { connected, hasError, total }
|
||||
})
|
||||
|
||||
const tooltipContent = createMemo(() => {
|
||||
const lsp = sync.data.lsp ?? []
|
||||
if (lsp.length === 0) return "No LSP servers"
|
||||
return lsp.map((s) => s.name).join(", ")
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={lspStats().total > 0}>
|
||||
<Tooltip placement="top" value={tooltipContent()}>
|
||||
<div class="flex items-center gap-1 px-2 cursor-default select-none">
|
||||
<Icon
|
||||
name="code"
|
||||
size="small"
|
||||
classList={{
|
||||
"text-icon-critical-base": lspStats().hasError,
|
||||
"text-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
|
||||
export function SessionMcpIndicator() {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
|
||||
const mcpStats = createMemo(() => {
|
||||
const mcp = sync.data.mcp ?? {}
|
||||
const entries = Object.entries(mcp)
|
||||
const enabled = entries.filter(([, status]) => status.status === "connected").length
|
||||
const failed = entries.some(([, status]) => status.status === "failed")
|
||||
const total = entries.length
|
||||
return { enabled, failed, total }
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={mcpStats().total > 0}>
|
||||
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
|
||||
<Icon
|
||||
name="mcp"
|
||||
size="small"
|
||||
classList={{
|
||||
"text-icon-critical-base": mcpStats().failed,
|
||||
"text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
|
||||
</Button>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Show, type ParentProps } from "solid-js"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
export function StatusBar(props: ParentProps) {
|
||||
const platform = usePlatform()
|
||||
return (
|
||||
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
|
||||
<Show when={platform.version}>
|
||||
<span class="text-12-regular text-text-weak">v{platform.version}</span>
|
||||
</Show>
|
||||
<div class="flex items-center">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineConfig } from "vite"
|
||||
import desktopPlugin from "./vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [desktopPlugin] as any,
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: true,
|
||||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
sourcemap: true,
|
||||
},
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.203",
|
||||
"version": "1.0.180",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/sst/opencode",
|
||||
starsFormatted: {
|
||||
compact: "41K",
|
||||
full: "41,000",
|
||||
compact: "38K",
|
||||
full: "38,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "450",
|
||||
commits: "6,000",
|
||||
contributors: "400",
|
||||
commits: "5,000",
|
||||
monthlyUsers: "400,000",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -6,7 +6,6 @@ const assetNames: Record<string, string> = {
|
||||
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
||||
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
||||
"linux-x64-deb": "opencode-desktop-linux-amd64.deb",
|
||||
"linux-x64-appimage": "opencode-desktop-linux-amd64.AppImage",
|
||||
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
||||
} satisfies Record<DownloadPlatform, string>
|
||||
|
||||
|
||||
@@ -244,22 +244,6 @@ export default function Download() {
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div data-component="download-row">
|
||||
<div data-component="download-info">
|
||||
<span data-slot="icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.34591 22.7088C5.61167 22.86 7.03384 23.6799 8.22401 23.8247C9.42058 23.9758 9.79086 23.0098 9.79086 23.0098C9.79086 23.0098 11.1374 22.7088 12.553 22.6741C13.97 22.6344 15.3113 22.9688 15.3113 22.9688C15.3113 22.9688 15.5714 23.5646 16.057 23.8247C16.5426 24.0898 17.588 24.1257 18.258 23.4198C18.9293 22.7088 20.7204 21.8132 21.7261 21.2533C22.7382 20.6922 22.5525 19.8364 21.917 19.5763C21.2816 19.3163 20.7614 18.9063 20.8011 18.1196C20.8357 17.3394 20.24 16.8193 20.24 16.8193C20.24 16.8193 20.7614 15.1025 20.2759 13.6805C19.7903 12.2648 18.1889 9.98819 16.9577 8.27657C15.7266 6.55985 16.7719 4.5779 15.651 2.04503C14.5299 -0.491656 11.623 -0.341713 10.0562 0.739505C8.4893 1.8208 8.96968 4.50225 9.04526 5.77447C9.12084 7.04022 9.07985 7.94598 8.93509 8.27146C8.79033 8.60198 7.77951 9.80243 7.1082 10.8081C6.43818 11.819 5.95254 13.906 5.46187 14.7669C4.98142 15.6228 5.31711 16.403 5.31711 16.403C5.31711 16.403 4.98149 16.5182 4.71628 17.0795C4.45616 17.6342 3.93601 17.8993 2.99948 18.0801C2.06934 18.2709 2.06934 18.8705 2.29357 19.5419C2.51902 20.2119 2.29357 20.5873 2.03346 21.4431C1.77342 22.2988 3.07506 22.5588 4.34591 22.7088ZM17.5034 18.805C18.1683 19.0958 19.124 18.691 19.4149 18.4001C19.7045 18.1106 19.9094 17.6801 19.9094 17.6801C19.9094 17.6801 20.2002 17.8249 20.1707 18.2848C20.14 18.7512 20.3706 19.4161 20.8062 19.6467C21.2418 19.876 21.9067 20.1963 21.5621 20.5166C21.211 20.8369 19.2688 21.6183 18.6885 22.2282C18.1132 22.8341 17.3573 23.33 16.8974 23.1839C16.4324 23.0391 16.0262 22.4037 16.2261 21.4736C16.4324 20.5473 16.6066 19.5313 16.5771 18.951C16.5464 18.3707 16.4324 17.5892 16.5771 17.4738C16.7219 17.3598 16.9525 17.4148 16.9525 17.4148C16.9525 17.4148 16.8371 18.5156 17.5034 18.805ZM13.1885 3.12632C13.829 3.12632 14.3454 3.76175 14.3454 4.54324C14.3454 5.09798 14.0853 5.57844 13.7048 5.80906C13.6087 5.76937 13.5087 5.72449 13.3986 5.67832C13.6292 5.56434 13.7893 5.27352 13.7893 4.93783C13.7893 4.49844 13.519 4.13714 13.1794 4.13714C12.8489 4.13714 12.5734 4.49836 12.5734 4.93783C12.5734 5.09806 12.6132 5.25813 12.6785 5.38369C12.4786 5.30293 12.298 5.23383 12.1532 5.17874C12.0776 4.98781 12.0328 4.77257 12.0328 4.54331C12.0328 3.76183 12.5478 3.12632 13.1885 3.12632ZM11.6024 5.56823C11.9176 5.62331 12.7835 5.9987 13.1039 6.11398C13.4242 6.22415 13.7791 6.4291 13.7445 6.63413C13.7048 6.84548 13.5395 6.84548 13.1039 7.1107C12.6735 7.37082 11.7331 7.95116 11.432 7.99085C11.1322 8.03055 10.9618 7.86141 10.6415 7.65516C10.3211 7.44503 9.72039 6.95436 9.87147 6.69432C9.87147 6.69432 10.3416 6.33432 10.5467 6.14986C10.7517 5.95893 11.2821 5.50925 11.6024 5.56823ZM10.2213 3.35185C10.726 3.35185 11.1373 3.95268 11.1373 4.69318C11.1373 4.82773 11.1219 4.95322 11.0976 5.07878C10.972 5.11847 10.8466 5.18385 10.726 5.28891C10.6671 5.33889 10.612 5.38369 10.5621 5.43367C10.6415 5.28381 10.6722 5.06857 10.6363 4.84305C10.5672 4.44335 10.2968 4.14743 10.0316 4.18712C9.76511 4.232 9.60625 4.5984 9.67033 5.00327C9.74081 5.41325 10.0059 5.7091 10.2763 5.6643C10.2917 5.6592 10.3058 5.65409 10.3211 5.64891C10.1918 5.77447 10.0713 5.88464 9.94576 5.97432C9.58065 5.80388 9.31033 5.29402 9.31033 4.69318C9.31041 3.94758 9.71521 3.35185 10.2213 3.35185ZM7.40915 13.045C7.9293 12.2251 8.26492 10.4328 8.78507 9.83702C9.31041 9.24259 9.71521 7.97554 9.53075 7.41569C9.53075 7.41569 10.6517 8.75702 11.432 8.53668C12.2135 8.31116 13.97 7.00571 14.23 7.22994C14.4901 7.45539 16.727 12.375 16.9525 13.9419C17.178 15.5074 16.8026 16.7041 16.8026 16.7041C16.8026 16.7041 15.9468 16.4785 15.8366 16.9987C15.7264 17.524 15.7264 19.4265 15.7264 19.4265C15.7264 19.4265 14.5695 21.0279 12.7784 21.2931C10.9874 21.5532 10.0905 21.3636 10.0905 21.3636L9.08481 20.2118C9.08481 20.2118 9.86637 20.0965 9.75612 19.3112C9.64595 18.531 7.36801 17.4496 6.95803 16.4785C6.5482 15.5073 6.8826 13.8662 7.40915 13.045ZM2.9802 18.9204C3.06988 18.5361 4.23056 18.5361 4.67643 18.2657C5.12229 17.9954 5.21189 17.219 5.57197 17.0141C5.92679 16.804 6.58279 17.5496 6.85311 17.9697C7.11833 18.3797 8.13433 20.1721 8.54942 20.6179C8.96961 21.0676 9.35528 21.6633 9.23483 22.1988C9.12084 22.7343 8.48923 23.1251 8.48923 23.1251C7.92427 23.2993 6.34843 22.619 5.63231 22.3192C4.9162 22.0182 3.09433 21.9284 2.8599 21.6633C2.61906 21.393 2.97517 20.7972 3.06995 20.2322C3.15445 19.6609 2.8893 19.306 2.9802 18.9204Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>Linux (.AppImage)</span>
|
||||
</div>
|
||||
<a href={getDownloadHref("linux-x64-appimage")} data-component="action-button">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
export type DownloadPlatform =
|
||||
| `darwin-${"x64" | "aarch64"}-dmg`
|
||||
| "windows-x64-nsis"
|
||||
| `linux-x64-${"deb" | "rpm" | "appimage"}`
|
||||
export type DownloadPlatform = `darwin-${"x64" | "aarch64"}-dmg` | "windows-x64-nsis" | `linux-x64-${"deb" | "rpm"}`
|
||||
|
||||
@@ -19,23 +19,17 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
|
||||
if (!data.modelName) return
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||
const year = timestamp.substring(0, 4)
|
||||
const month = timestamp.substring(4, 6)
|
||||
const day = timestamp.substring(6, 8)
|
||||
const hour = timestamp.substring(8, 10)
|
||||
const minute = timestamp.substring(10, 12)
|
||||
const second = timestamp.substring(12, 14)
|
||||
|
||||
waitUntil(
|
||||
Resource.ZenDataNew.put(
|
||||
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
|
||||
Resource.ZenData.put(
|
||||
`data/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...data }),
|
||||
),
|
||||
)
|
||||
|
||||
waitUntil(
|
||||
Resource.ZenDataNew.put(
|
||||
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||
Resource.ZenData.put(
|
||||
`meta/${data.modelName}/${timestamp}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...metadata }),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.203",
|
||||
"version": "1.0.180",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -17,16 +17,14 @@ const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||
@@ -34,4 +32,3 @@ await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
|
||||
|
||||
@@ -17,15 +17,14 @@ const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||
@@ -33,4 +32,3 @@ await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6}`
|
||||
|
||||
@@ -15,20 +15,16 @@ const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=
|
||||
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `models-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(
|
||||
JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6), null, 2),
|
||||
)
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5), null, 2))
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
// open temp file in vim and read the file on close
|
||||
@@ -37,17 +33,15 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
ZenData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
const chunk = Math.ceil(newValue.length / 6)
|
||||
const chunk = Math.ceil(newValue.length / 5)
|
||||
const newValue1 = newValue.slice(0, chunk)
|
||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
|
||||
const newValue5 = newValue.slice(chunk * 4, chunk * 5)
|
||||
const newValue6 = newValue.slice(chunk * 5)
|
||||
const newValue5 = newValue.slice(chunk * 4)
|
||||
|
||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
|
||||
|
||||
@@ -72,8 +72,7 @@ export namespace ZenData {
|
||||
Resource.ZEN_MODELS2.value +
|
||||
Resource.ZEN_MODELS3.value +
|
||||
Resource.ZEN_MODELS4.value +
|
||||
Resource.ZEN_MODELS5.value +
|
||||
Resource.ZEN_MODELS6.value,
|
||||
Resource.ZEN_MODELS5.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
||||
13
packages/console/core/sst-env.d.ts
vendored
13
packages/console/core/sst-env.d.ts
vendored
@@ -42,6 +42,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,10 +98,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
"WebApp": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -118,10 +118,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS6": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
@@ -136,7 +132,6 @@ declare module "sst" {
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
"ZenDataNew": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.203",
|
||||
"version": "1.0.180",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
13
packages/console/function/sst-env.d.ts
vendored
13
packages/console/function/sst-env.d.ts
vendored
@@ -42,6 +42,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,10 +98,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
"WebApp": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -118,10 +118,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS6": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
@@ -136,7 +132,6 @@ declare module "sst" {
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
"ZenDataNew": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.203",
|
||||
"version": "1.0.180",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
13
packages/console/resource/sst-env.d.ts
vendored
13
packages/console/resource/sst-env.d.ts
vendored
@@ -42,6 +42,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,10 +98,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
"WebApp": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -118,10 +118,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS6": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
@@ -136,7 +132,6 @@ declare module "sst" {
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
"ZenDataNew": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
packages/desktop/.gitignore
vendored
25
packages/desktop/.gitignore
vendored
@@ -1,24 +1 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
src/assets/theme.css
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
# Tauri + Vanilla TS
|
||||
## Usage
|
||||
|
||||
This template should help get you started developing with Tauri in vanilla HTML, CSS and Typescript.
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
## Recommended IDE Setup
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
@@ -23,6 +23,6 @@
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,37 +1,62 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.203",
|
||||
"version": "1.0.180",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./vite": "./vite.js"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
"predev": "bun ./scripts/predev.ts",
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "bun run typecheck && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "20.0.11",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||
"vite-plugin-solid": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
"@tauri-apps/plugin-http": "~2",
|
||||
"@tauri-apps/plugin-window-state": "~2",
|
||||
"solid-js": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "4.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "catalog:"
|
||||
"@solid-primitives/websocket": "1.3.1",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.3.0",
|
||||
"luxon": "catalog:",
|
||||
"marked": "16.2.0",
|
||||
"marked-shiki": "1.2.1",
|
||||
"remeda": "catalog:",
|
||||
"shiki": "3.9.2",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"virtua": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import { $ } from "bun"
|
||||
|
||||
import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
|
||||
|
||||
const sidecarConfig = getCurrentSidecar()
|
||||
|
||||
const dir = "src-tauri/target/opencode-binaries"
|
||||
|
||||
await $`mkdir -p ${dir}`
|
||||
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
|
||||
|
||||
await copyBinaryToSidecarFolder(
|
||||
`${dir}/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`,
|
||||
)
|
||||
@@ -21,7 +21,6 @@ import Home from "@/pages/home"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Session from "@/pages/session"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -29,17 +28,14 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const url = iife(() => {
|
||||
const param = new URLSearchParams(document.location.search).get("url")
|
||||
if (param) return param
|
||||
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
||||
const port = window.__OPENCODE__?.port ?? import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
||||
|
||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
|
||||
if (import.meta.env.DEV)
|
||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||
|
||||
return window.location.origin
|
||||
})
|
||||
const url =
|
||||
new URLSearchParams(document.location.search).get("url") ||
|
||||
(location.hostname.includes("opencode.ai") || location.hostname.includes("localhost")
|
||||
? `http://${host}:${port}`
|
||||
: "/")
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Link } from "@/components/link"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { DialogSelectModel } from "./dialog-select-model"
|
||||
import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Link } from "@/components/link"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogSelectModel } from "./dialog-select-model"
|
||||
|
||||
export function DialogConnectProvider(props: { provider: string }) {
|
||||
const dialog = useDialog()
|
||||
@@ -154,9 +154,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
||||
<div class="">
|
||||
<List
|
||||
ref={(ref) => {
|
||||
listRef = ref
|
||||
}}
|
||||
ref={(ref) => (listRef = ref)}
|
||||
items={methods}
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (method, index) => {
|
||||
@@ -165,7 +163,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2">
|
||||
<div class="w-full flex items-center gap-x-4">
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
@@ -177,7 +175,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
</Match>
|
||||
<Match when={store.state === "pending"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<Spinner />
|
||||
<span>Authorization in progress...</span>
|
||||
</div>
|
||||
@@ -185,7 +183,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
</Match>
|
||||
<Match when={store.state === "error"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||
<span>Authorization failed: {store.error}</span>
|
||||
</div>
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import type { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
|
||||
export const DialogManageModels: Component = () => {
|
||||
const local = useLocal()
|
||||
return (
|
||||
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||
@@ -26,24 +27,16 @@ export const DialogManageModels: Component = () => {
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
const visible = local.model.visible({
|
||||
modelID: x.id,
|
||||
providerID: x.provider.id,
|
||||
})
|
||||
const visible = local.model.visible({ modelID: x.id, providerID: x.provider.id })
|
||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between gap-x-3">
|
||||
<div class="w-full flex items-center justify-between gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
checked={
|
||||
!!local.model.visible({
|
||||
modelID: i.id,
|
||||
providerID: i.provider.id,
|
||||
})
|
||||
}
|
||||
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
||||
onChange={(checked) => {
|
||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||
}}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const layout = useLayout()
|
||||
@@ -18,6 +18,7 @@ export function DialogSelectFile() {
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={local.file.searchFiles}
|
||||
@@ -31,7 +32,7 @@ export function DialogSelectFile() {
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<div class="flex items-center gap-x-2 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { type Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
|
||||
export const DialogSelectModelUnpaid: Component = () => {
|
||||
const local = useLocal()
|
||||
@@ -64,7 +64,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
||||
<div class="w-full">
|
||||
<List
|
||||
class="w-full px-0"
|
||||
class="w-full"
|
||||
key={(x) => x?.id}
|
||||
items={providers.popular}
|
||||
activeIcon="plus-small"
|
||||
@@ -79,8 +79,17 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<div class="w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
data-slot="list-item-extra-icon"
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
@@ -35,6 +35,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
}
|
||||
>
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
@@ -60,7 +61,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-3">
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||
<Tag>Free</Tag>
|
||||
@@ -15,6 +15,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
return (
|
||||
<Dialog title="Connect provider">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search providers", autofocus: true }}
|
||||
activeIcon="plus-small"
|
||||
key={(x) => x?.id}
|
||||
@@ -37,8 +38,17 @@ export const DialogSelectProvider: Component = () => {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<div class="px-1.25 w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
data-slot="list-item-extra-icon"
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
@@ -2,7 +2,7 @@ import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
|
||||
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
|
||||
export default function FileTree(props: {
|
||||
@@ -57,14 +57,14 @@ export default function FileTree(props: {
|
||||
"text-text-muted/40": p.node.ignored,
|
||||
"text-text-muted/80": !p.node.ignored,
|
||||
// "!text-text": local.file.active()?.path === p.node.path,
|
||||
// "!text-primary": local.file.changed(p.node.path),
|
||||
"!text-primary": local.file.changed(p.node.path),
|
||||
}}
|
||||
>
|
||||
{p.node.name}
|
||||
</span>
|
||||
{/* <Show when={local.file.changed(p.node.path)}> */}
|
||||
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
|
||||
{/* </Show> */}
|
||||
<Show when={local.file.changed(p.node.path)}>
|
||||
<span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" />
|
||||
</Show>
|
||||
</Dynamic>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Select } from "@opencode-ai/ui/select"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
@@ -20,29 +19,19 @@ import { iife } from "@opencode-ai/util/iife"
|
||||
export function Header(props: {
|
||||
navigateToProject: (directory: string) => void
|
||||
navigateToSession: (session: Session | undefined) => void
|
||||
onMobileMenuToggle?: () => void
|
||||
}) {
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const command = useCommand()
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
onClick={props.onMobileMenuToggle}
|
||||
>
|
||||
<Icon name="menu" size="small" />
|
||||
</button>
|
||||
<A
|
||||
href="/"
|
||||
classList={{
|
||||
"hidden xl:flex": true,
|
||||
"w-12 shrink-0 px-4 py-3.5": true,
|
||||
"items-center justify-start self-stretch": true,
|
||||
"flex items-center justify-start self-stretch": true,
|
||||
"border-r border-border-weak-base": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
@@ -55,32 +44,30 @@ export function Header(props: {
|
||||
{(directory) => {
|
||||
const currentDirectory = createMemo(() => base64Decode(directory()))
|
||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||
const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
|
||||
const sessions = createMemo(() => store().session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => store().config.share !== "disabled")
|
||||
return (
|
||||
<>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
@@ -88,64 +75,23 @@ export function Header(props: {
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={props.navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
class="text-14-regular text-text-base max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Tooltip
|
||||
class="hidden xl:block"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>New session</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<Tooltip
|
||||
class="hidden md:block shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle review</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Tooltip
|
||||
class="hidden md:block shrink-0"
|
||||
class="shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
|
||||
<span class="text-icon-base text-12-medium">Ctrl `</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -19,10 +19,9 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useCommand, formatKeybind } from "@/context/command"
|
||||
import { persisted } from "@/utils/persist"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||
@@ -103,6 +102,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
imageAttachments: ImageAttachmentPart[]
|
||||
mode: "normal" | "shell"
|
||||
applyingHistory: boolean
|
||||
userHasEdited: boolean
|
||||
}>({
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
@@ -112,6 +112,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
imageAttachments: [],
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
userHasEdited: false,
|
||||
})
|
||||
|
||||
const MAX_HISTORY = 100
|
||||
@@ -148,6 +149,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
|
||||
const length = position === "start" ? 0 : promptLength(p)
|
||||
setStore("applyingHistory", true)
|
||||
setStore("userHasEdited", false)
|
||||
prompt.set(p, length)
|
||||
requestAnimationFrame(() => {
|
||||
editorRef.focus()
|
||||
@@ -276,7 +278,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!isFocused()) setStore("popover", null)
|
||||
if (isFocused()) {
|
||||
handleInput()
|
||||
} else {
|
||||
setStore("popover", null)
|
||||
}
|
||||
})
|
||||
|
||||
const handleFileSelect = (path: string | undefined) => {
|
||||
@@ -356,12 +362,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
() => prompt.current(),
|
||||
(currentParts) => {
|
||||
const domParts = parseFromDOM()
|
||||
const normalized = Array.from(editorRef.childNodes).every((node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) return true
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return false
|
||||
return (node as HTMLElement).dataset.type === "file"
|
||||
})
|
||||
if (normalized && isPromptEqual(currentParts, domParts)) return
|
||||
if (isPromptEqual(currentParts, domParts)) return
|
||||
|
||||
const selection = window.getSelection()
|
||||
let cursorPosition: number | null = null
|
||||
@@ -393,21 +394,34 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
)
|
||||
|
||||
const parseFromDOM = (): Prompt => {
|
||||
const parts: Prompt = []
|
||||
const newParts: Prompt = []
|
||||
let position = 0
|
||||
let buffer = ""
|
||||
|
||||
const flushText = () => {
|
||||
const content = buffer.replace(/\r\n?/g, "\n")
|
||||
buffer = ""
|
||||
const pushText = (content: string) => {
|
||||
if (!content) return
|
||||
parts.push({ type: "text", content, start: position, end: position + content.length })
|
||||
newParts.push({ type: "text", content, start: position, end: position + content.length })
|
||||
position += content.length
|
||||
}
|
||||
|
||||
const pushFile = (file: HTMLElement) => {
|
||||
const rangeText = (range: Range) => {
|
||||
const fragment = range.cloneContents()
|
||||
const container = document.createElement("div")
|
||||
container.append(fragment)
|
||||
return container.innerText
|
||||
}
|
||||
|
||||
const files = Array.from(editorRef.querySelectorAll<HTMLElement>("[data-type=file]"))
|
||||
let last: HTMLElement | undefined
|
||||
|
||||
files.forEach((file) => {
|
||||
const before = document.createRange()
|
||||
before.selectNodeContents(editorRef)
|
||||
if (last) before.setStartAfter(last)
|
||||
before.setEndBefore(file)
|
||||
pushText(rangeText(before))
|
||||
|
||||
const content = file.textContent ?? ""
|
||||
parts.push({
|
||||
newParts.push({
|
||||
type: "file",
|
||||
path: file.dataset.path!,
|
||||
content,
|
||||
@@ -415,44 +429,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
end: position + content.length,
|
||||
})
|
||||
position += content.length
|
||||
}
|
||||
|
||||
const visit = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
buffer += node.textContent ?? ""
|
||||
return
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return
|
||||
|
||||
const el = node as HTMLElement
|
||||
if (el.dataset.type === "file") {
|
||||
flushText()
|
||||
pushFile(el)
|
||||
return
|
||||
}
|
||||
if (el.tagName === "BR") {
|
||||
buffer += "\n"
|
||||
return
|
||||
}
|
||||
|
||||
for (const child of Array.from(el.childNodes)) {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
|
||||
const children = Array.from(editorRef.childNodes)
|
||||
children.forEach((child, index) => {
|
||||
const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName)
|
||||
visit(child)
|
||||
if (isBlock && index < children.length - 1) {
|
||||
buffer += "\n"
|
||||
}
|
||||
last = file
|
||||
})
|
||||
|
||||
flushText()
|
||||
const after = document.createRange()
|
||||
after.selectNodeContents(editorRef)
|
||||
if (last) after.setStartAfter(last)
|
||||
pushText(rangeText(after))
|
||||
|
||||
if (parts.length === 0) parts.push(...DEFAULT_PROMPT)
|
||||
return parts
|
||||
if (newParts.length === 0) newParts.push(...DEFAULT_PROMPT)
|
||||
return newParts
|
||||
}
|
||||
|
||||
const handleInput = () => {
|
||||
@@ -465,6 +451,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
if (shouldReset) {
|
||||
setStore("popover", null)
|
||||
setStore("userHasEdited", false)
|
||||
if (store.historyIndex >= 0 && !store.applyingHistory) {
|
||||
setStore("historyIndex", -1)
|
||||
setStore("savedPrompt", null)
|
||||
@@ -499,6 +486,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
setStore("savedPrompt", null)
|
||||
}
|
||||
|
||||
if (!store.applyingHistory) {
|
||||
setStore("userHasEdited", true)
|
||||
}
|
||||
|
||||
prompt.set(rawParts, cursorPosition)
|
||||
}
|
||||
|
||||
@@ -524,37 +515,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const gap = document.createTextNode(" ")
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
const setEdge = (edge: "start" | "end", offset: number) => {
|
||||
let remaining = offset
|
||||
const nodes = Array.from(editorRef.childNodes)
|
||||
|
||||
for (const node of nodes) {
|
||||
const length = node.textContent?.length ?? 0
|
||||
const isText = node.nodeType === Node.TEXT_NODE
|
||||
const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
|
||||
|
||||
if (isText && remaining <= length) {
|
||||
if (edge === "start") range.setStart(node, remaining)
|
||||
if (edge === "end") range.setEnd(node, remaining)
|
||||
return
|
||||
}
|
||||
|
||||
if (isFile && remaining <= length) {
|
||||
if (edge === "start" && remaining === 0) range.setStartBefore(node)
|
||||
if (edge === "start" && remaining > 0) range.setStartAfter(node)
|
||||
if (edge === "end" && remaining === 0) range.setEndBefore(node)
|
||||
if (edge === "end" && remaining > 0) range.setEndAfter(node)
|
||||
return
|
||||
}
|
||||
|
||||
remaining -= length
|
||||
}
|
||||
}
|
||||
|
||||
if (atMatch) {
|
||||
const start = atMatch.index ?? cursorPosition - atMatch[0].length
|
||||
setEdge("start", start)
|
||||
setEdge("end", cursorPosition)
|
||||
let runningLength = 0
|
||||
|
||||
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
|
||||
let currentNode = walker.nextNode()
|
||||
while (currentNode) {
|
||||
const textContent = currentNode.textContent || ""
|
||||
if (runningLength + textContent.length >= atMatch.index!) {
|
||||
const localStart = atMatch.index! - runningLength
|
||||
const localEnd = cursorPosition - runningLength
|
||||
if (currentNode === range.startContainer || runningLength + textContent.length >= cursorPosition) {
|
||||
range.setStart(currentNode, localStart)
|
||||
range.setEnd(currentNode, Math.min(localEnd, textContent.length))
|
||||
break
|
||||
}
|
||||
}
|
||||
runningLength += textContent.length
|
||||
currentNode = walker.nextNode()
|
||||
}
|
||||
}
|
||||
|
||||
range.deleteContents()
|
||||
@@ -604,6 +583,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
const navigateHistory = (direction: "up" | "down") => {
|
||||
if (store.userHasEdited) return false
|
||||
|
||||
const entries = store.mode === "shell" ? shellHistory.entries : history.entries
|
||||
const current = store.historyIndex
|
||||
|
||||
@@ -710,11 +691,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && event.shiftKey) {
|
||||
addPart({ type: "text", content: "\n", start: 0, end: 0 })
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
handleSubmit(event)
|
||||
}
|
||||
@@ -740,6 +716,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
addToHistory(currentPrompt, store.mode)
|
||||
setStore("historyIndex", -1)
|
||||
setStore("savedPrompt", null)
|
||||
setStore("userHasEdited", false)
|
||||
|
||||
let existing = info()
|
||||
if (!existing) {
|
||||
@@ -912,8 +889,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
custom
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={command.keybind(cmd.id)}>
|
||||
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span>
|
||||
<Show when={cmd.keybind}>
|
||||
<span class="text-12-regular text-text-subtle">{formatKeybind(cmd.keybind!)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
@@ -989,21 +966,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
classList={{
|
||||
"w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||
"[&_[data-type=file]]:text-icon-info-active": true,
|
||||
"w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||
"[&>[data-type=file]]:text-icon-info-active": true,
|
||||
"font-mono!": store.mode === "shell",
|
||||
}}
|
||||
/>
|
||||
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
|
||||
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
|
||||
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
||||
{store.mode === "shell"
|
||||
? "Enter shell command..."
|
||||
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="absolute top-4.5 right-4">
|
||||
<SessionContextUsage />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative p-3 flex items-center justify-between">
|
||||
<div class="flex items-center justify-start gap-1">
|
||||
@@ -1016,48 +990,26 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Cycle agent</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("agent.cycle")}</span>
|
||||
</div>
|
||||
<Select
|
||||
options={local.agent.list().map((agent) => agent.name)}
|
||||
current={local.agent.current().name}
|
||||
onSelect={local.agent.set}
|
||||
class="capitalize"
|
||||
variant="ghost"
|
||||
/>
|
||||
<Button
|
||||
as="div"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
dialog.show(() =>
|
||||
providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Select
|
||||
options={local.agent.list().map((agent) => agent.name)}
|
||||
current={local.agent.current().name}
|
||||
onSelect={local.agent.set}
|
||||
class="capitalize"
|
||||
variant="ghost"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Choose model</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("model.choose")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="div"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
dialog.show(() =>
|
||||
providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
|
||||
)
|
||||
}
|
||||
>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
<span class="hidden md:block ml-0.5 text-text-weak text-12-regular">
|
||||
{local.model.current()?.provider.name}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
@@ -119,6 +119,7 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search commands", autofocus: true }}
|
||||
emptyMessage="No commands found"
|
||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||
@@ -225,11 +226,6 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
}
|
||||
}
|
||||
},
|
||||
keybind(id: string) {
|
||||
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
||||
if (!option?.keybind) return ""
|
||||
return formatKeybind(option.keybind)
|
||||
},
|
||||
show: showPalette,
|
||||
keybinds(enabled: boolean) {
|
||||
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
type Part,
|
||||
type Config,
|
||||
type Path,
|
||||
type File,
|
||||
type FileNode,
|
||||
type Project,
|
||||
type FileDiff,
|
||||
type Todo,
|
||||
@@ -12,8 +14,6 @@ import {
|
||||
type ProviderListResponse,
|
||||
type ProviderAuthResponse,
|
||||
type Command,
|
||||
type McpStatus,
|
||||
type LspStatus,
|
||||
createOpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
@@ -43,10 +43,6 @@ type State = {
|
||||
todo: {
|
||||
[sessionID: string]: Todo[]
|
||||
}
|
||||
mcp: {
|
||||
[name: string]: McpStatus
|
||||
}
|
||||
lsp: LspStatus[]
|
||||
limit: number
|
||||
message: {
|
||||
[sessionID: string]: Message[]
|
||||
@@ -54,6 +50,8 @@ type State = {
|
||||
part: {
|
||||
[messageID: string]: Part[]
|
||||
}
|
||||
node: FileNode[]
|
||||
changes: File[]
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
@@ -91,11 +89,11 @@ function createGlobalSync() {
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
node: [],
|
||||
changes: [],
|
||||
})
|
||||
children[directory] = createStore(globalStore.children[directory])
|
||||
bootstrapInstance(directory)
|
||||
@@ -138,27 +136,15 @@ function createGlobalSync() {
|
||||
})
|
||||
const load = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
sdk.provider.list().then((x) => {
|
||||
const data = x.data!
|
||||
setStore("provider", {
|
||||
...data,
|
||||
all: data.all.map((provider) => ({
|
||||
...provider,
|
||||
models: Object.fromEntries(
|
||||
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
|
||||
),
|
||||
})),
|
||||
})
|
||||
}),
|
||||
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
|
||||
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
session: () => loadSessions(directory),
|
||||
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})),
|
||||
lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])),
|
||||
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
|
||||
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
||||
.then(() => setStore("ready", true))
|
||||
@@ -225,13 +211,13 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "session.diff":
|
||||
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
|
||||
setStore("session_diff", event.properties.sessionID, event.properties.diff)
|
||||
break
|
||||
case "todo.updated":
|
||||
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
|
||||
setStore("todo", event.properties.sessionID, event.properties.todos)
|
||||
break
|
||||
case "session.status": {
|
||||
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
|
||||
setStore("session_status", event.properties.sessionID, event.properties.status)
|
||||
break
|
||||
}
|
||||
case "message.updated": {
|
||||
@@ -309,18 +295,6 @@ function createGlobalSync() {
|
||||
})
|
||||
|
||||
async function bootstrap() {
|
||||
const health = await globalSDK.client.global
|
||||
.health()
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
if (!health?.healthy) {
|
||||
setGlobalStore(
|
||||
"error",
|
||||
new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
retry(() =>
|
||||
globalSDK.client.path.get().then((x) => {
|
||||
@@ -337,16 +311,7 @@ function createGlobalSync() {
|
||||
),
|
||||
retry(() =>
|
||||
globalSDK.client.provider.list().then((x) => {
|
||||
const data = x.data!
|
||||
setGlobalStore("provider", {
|
||||
...data,
|
||||
all: data.all.map((provider) => ({
|
||||
...provider,
|
||||
models: Object.fromEntries(
|
||||
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
|
||||
),
|
||||
})),
|
||||
})
|
||||
setGlobalStore("provider", x.data ?? {})
|
||||
}),
|
||||
),
|
||||
retry(() =>
|
||||
@@ -47,10 +47,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
height: 280,
|
||||
},
|
||||
review: {
|
||||
opened: true,
|
||||
},
|
||||
session: {
|
||||
width: 600,
|
||||
state: "pane" as "pane" | "tab",
|
||||
},
|
||||
sessionTabs: {} as Record<string, SessionTabs>,
|
||||
}),
|
||||
@@ -111,12 +108,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
|
||||
},
|
||||
expand(directory: string) {
|
||||
const index = store.projects.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", index, "expanded", true)
|
||||
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const index = store.projects.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", index, "expanded", false)
|
||||
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
setStore("projects", (projects) => {
|
||||
@@ -162,25 +157,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
},
|
||||
review: {
|
||||
opened: createMemo(() => store.review?.opened ?? true),
|
||||
open() {
|
||||
setStore("review", "opened", true)
|
||||
state: createMemo(() => store.review?.state ?? "closed"),
|
||||
pane() {
|
||||
setStore("review", "state", "pane")
|
||||
},
|
||||
close() {
|
||||
setStore("review", "opened", false)
|
||||
},
|
||||
toggle() {
|
||||
setStore("review", "opened", (x) => !x)
|
||||
},
|
||||
},
|
||||
session: {
|
||||
width: createMemo(() => store.session?.width ?? 600),
|
||||
resize(width: number) {
|
||||
if (!store.session) {
|
||||
setStore("session", { width })
|
||||
} else {
|
||||
setStore("session", "width", width)
|
||||
}
|
||||
tab() {
|
||||
setStore("review", "state", "tab")
|
||||
},
|
||||
},
|
||||
tabs(sessionKey: string) {
|
||||
@@ -204,6 +186,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
},
|
||||
async open(tab: string) {
|
||||
if (tab === "chat") {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [], active: undefined })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "active", undefined)
|
||||
}
|
||||
return
|
||||
}
|
||||
const current = store.sessionTabs[sessionKey] ?? { all: [] }
|
||||
if (tab !== "review") {
|
||||
if (!current.all.includes(tab)) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { batch, createEffect, createMemo } from "solid-js"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
@@ -9,7 +9,6 @@ import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { DateTime } from "luxon"
|
||||
import { persisted } from "@/utils/persist"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
export type LocalFile = FileNode &
|
||||
Partial<{
|
||||
@@ -62,6 +61,24 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically update model when agent changes
|
||||
createEffect(() => {
|
||||
const value = agent.current()
|
||||
if (value.model) {
|
||||
if (isModelValid(value.model))
|
||||
model.set({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
// else
|
||||
// toast.show({
|
||||
// type: "warning",
|
||||
// message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
|
||||
// duration: 3000,
|
||||
// })
|
||||
}
|
||||
})
|
||||
|
||||
const agent = (() => {
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const [store, setStore] = createStore<{
|
||||
@@ -259,11 +276,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const [store, setStore] = createStore<{
|
||||
node: Record<string, LocalFile>
|
||||
}>({
|
||||
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
||||
node: Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
||||
})
|
||||
|
||||
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
||||
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
||||
const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
||||
const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
||||
|
||||
// createEffect((prev: FileStatus[]) => {
|
||||
// const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
|
||||
@@ -291,16 +308,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
// return sync.data.changes
|
||||
// }, sync.data.changes)
|
||||
|
||||
// const changed = (path: string) => {
|
||||
// const node = store.node[path]
|
||||
// if (node?.status) return true
|
||||
// const set = changeset()
|
||||
// if (set.has(path)) return true
|
||||
// for (const p of set) {
|
||||
// if (p.startsWith(path ? path + "/" : "")) return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
const changed = (path: string) => {
|
||||
const node = store.node[path]
|
||||
if (node?.status) return true
|
||||
const set = changeset()
|
||||
if (set.has(path)) return true
|
||||
for (const p of set) {
|
||||
if (p.startsWith(path ? path + "/" : "")) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// const resetNode = (path: string) => {
|
||||
// setStore("node", path, {
|
||||
@@ -319,26 +336,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const load = async (path: string) => {
|
||||
const relativePath = relative(path)
|
||||
await sdk.client.file
|
||||
.read({ path: relativePath })
|
||||
.then((x) => {
|
||||
if (!store.node[relativePath]) return
|
||||
setStore(
|
||||
"node",
|
||||
relativePath,
|
||||
produce((draft) => {
|
||||
draft.loaded = true
|
||||
draft.content = x.data
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Failed to load file",
|
||||
description: e.message,
|
||||
})
|
||||
})
|
||||
await sdk.client.file.read({ path: relativePath }).then((x) => {
|
||||
if (!store.node[relativePath]) return
|
||||
setStore(
|
||||
"node",
|
||||
relativePath,
|
||||
produce((draft) => {
|
||||
draft.loaded = true
|
||||
draft.content = x.data
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const fetch = async (path: string) => {
|
||||
@@ -352,7 +360,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const init = async (path: string) => {
|
||||
const relativePath = relative(path)
|
||||
if (!store.node[relativePath]) await fetch(path)
|
||||
if (store.node[relativePath]?.loaded) return
|
||||
if (store.node[relativePath].loaded) return
|
||||
return load(relativePath)
|
||||
}
|
||||
|
||||
@@ -372,7 +380,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
context.addActive()
|
||||
if (options?.pinned) setStore("node", path, "pinned", true)
|
||||
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
|
||||
if (store.node[relativePath]?.loaded) return
|
||||
if (store.node[relativePath].loaded) return
|
||||
return load(relativePath)
|
||||
}
|
||||
|
||||
@@ -458,8 +466,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setChangeIndex(path: string, index: number | undefined) {
|
||||
setStore("node", path, "selectedChange", index)
|
||||
},
|
||||
// changes,
|
||||
// changed,
|
||||
changes,
|
||||
changed,
|
||||
children(path: string) {
|
||||
return Object.values(store.node).filter(
|
||||
(x) =>
|
||||
@@ -5,9 +5,6 @@ export type Platform = {
|
||||
/** Platform discriminator */
|
||||
platform: "web" | "tauri"
|
||||
|
||||
/** App version */
|
||||
version?: string
|
||||
|
||||
/** Open a URL in the default browser */
|
||||
openLink(url: string): void
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { render } from "solid-js/web"
|
||||
import { App } from "@/app"
|
||||
import { Platform, PlatformProvider } from "@/context/platform"
|
||||
import pkg from "../package.json"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
@@ -13,7 +12,6 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
|
||||
const platform: Platform = {
|
||||
platform: "web",
|
||||
version: pkg.version,
|
||||
openLink(url: string) {
|
||||
window.open(url, "_blank")
|
||||
},
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Component, Show } from "solid-js"
|
||||
import { Component } from "solid-js"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
|
||||
@@ -62,54 +62,12 @@ function formatInitError(error: InitError): string {
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): string {
|
||||
if (!error) return "Unknown error"
|
||||
|
||||
if (isInitError(error)) {
|
||||
const message = formatInitError(error)
|
||||
if (depth > 0 && parentMessage === message) return ""
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
return indent + message
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const isDuplicate = depth > 0 && parentMessage === error.message
|
||||
const parts: string[] = []
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
|
||||
if (!isDuplicate) {
|
||||
// Stack already includes error name and message, so prefer it
|
||||
parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
|
||||
} else if (error.stack) {
|
||||
// Duplicate message - only show the stack trace lines (skip message)
|
||||
const trace = error.stack.split("\n").slice(1).join("\n").trim()
|
||||
if (trace) {
|
||||
parts.push(trace)
|
||||
}
|
||||
}
|
||||
|
||||
if (error.cause) {
|
||||
const causeResult = formatErrorChain(error.cause, depth + 1, error.message)
|
||||
if (causeResult) {
|
||||
parts.push(causeResult)
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
if (depth > 0 && parentMessage === error) return ""
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
return indent + error
|
||||
}
|
||||
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
return indent + JSON.stringify(error, null, 2)
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return formatErrorChain(error, 0)
|
||||
if (!error) return "Unknown error"
|
||||
if (isInitError(error)) return formatInitError(error)
|
||||
if (error instanceof Error) return `${error.name}: ${error.message}\n\n${error.stack}`
|
||||
if (typeof error === "string") return error
|
||||
return JSON.stringify(error, null, 2)
|
||||
}
|
||||
|
||||
interface ErrorPageProps {
|
||||
@@ -138,21 +96,16 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
<Button size="large" onClick={platform.restart}>
|
||||
Restart
|
||||
</Button>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
Please report this error to the OpenCode team
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center text-text-interactive-base gap-1"
|
||||
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
>
|
||||
<div>on Discord</div>
|
||||
<Icon name="discord" class="text-text-interactive-base" />
|
||||
</button>
|
||||
</div>
|
||||
<Show when={platform.version}>
|
||||
<p class="text-xs text-text-weak">Version: {platform.version}</p>
|
||||
</Show>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
Please report this error to the OpenCode team
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center text-text-interactive-base gap-1"
|
||||
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
>
|
||||
<div>on Discord</div>
|
||||
<Icon name="discord" class="text-text-interactive-base" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,17 +1,4 @@
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
ParentProps,
|
||||
Show,
|
||||
Switch,
|
||||
untrack,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { createEffect, createMemo, For, Match, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
||||
@@ -55,29 +42,9 @@ export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = createStore({
|
||||
lastSession: {} as { [directory: string]: string },
|
||||
activeDraggable: undefined as string | undefined,
|
||||
mobileSidebarOpen: false,
|
||||
mobileProjectsExpanded: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
const mobileSidebar = {
|
||||
open: () => store.mobileSidebarOpen,
|
||||
show: () => setStore("mobileSidebarOpen", true),
|
||||
hide: () => setStore("mobileSidebarOpen", false),
|
||||
toggle: () => setStore("mobileSidebarOpen", (x) => !x),
|
||||
}
|
||||
|
||||
const mobileProjects = {
|
||||
expanded: (directory: string) => store.mobileProjectsExpanded[directory] ?? true,
|
||||
expand: (directory: string) => setStore("mobileProjectsExpanded", directory, true),
|
||||
collapse: (directory: string) => setStore("mobileProjectsExpanded", directory, false),
|
||||
}
|
||||
|
||||
let scrollContainerRef: HTMLDivElement | undefined
|
||||
const xlQuery = window.matchMedia("(min-width: 1280px)")
|
||||
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
|
||||
const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
|
||||
xlQuery.addEventListener("change", handleViewportChange)
|
||||
onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange))
|
||||
|
||||
const params = useParams()
|
||||
const globalSDK = useGlobalSDK()
|
||||
@@ -117,31 +84,42 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
})
|
||||
|
||||
function sortSessions(a: Session, b: Session) {
|
||||
const now = Date.now()
|
||||
const oneMinuteAgo = now - 60 * 1000
|
||||
const aUpdated = a.time.updated ?? a.time.created
|
||||
const bUpdated = b.time.updated ?? b.time.created
|
||||
const aRecent = aUpdated > oneMinuteAgo
|
||||
const bRecent = bUpdated > oneMinuteAgo
|
||||
if (aRecent && bRecent) return a.id.localeCompare(b.id)
|
||||
if (aRecent && !bRecent) return -1
|
||||
if (!aRecent && bRecent) return 1
|
||||
return bUpdated - aUpdated
|
||||
function flattenSessions(sessions: Session[]): Session[] {
|
||||
const childrenMap = new Map<string, Session[]>()
|
||||
for (const session of sessions) {
|
||||
if (session.parentID) {
|
||||
const children = childrenMap.get(session.parentID) ?? []
|
||||
children.push(session)
|
||||
childrenMap.set(session.parentID, children)
|
||||
}
|
||||
}
|
||||
const result: Session[] = []
|
||||
function visit(session: Session) {
|
||||
result.push(session)
|
||||
for (const child of childrenMap.get(session.id) ?? []) {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
for (const session of sessions) {
|
||||
if (!session.parentID) visit(session)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function scrollToSession(sessionId: string) {
|
||||
if (!scrollContainerRef) return
|
||||
const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
|
||||
if (element) {
|
||||
element.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||
element.scrollIntoView({ block: "center", behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
|
||||
function projectSessions(directory: string) {
|
||||
if (!directory) return []
|
||||
const sessions = globalSync.child(directory)[0].session.toSorted(sortSessions)
|
||||
return (sessions ?? []).filter((s) => !s.parentID)
|
||||
const sessions = globalSync
|
||||
.child(directory)[0]
|
||||
.session.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
return flattenSessions(sessions ?? [])
|
||||
}
|
||||
|
||||
const currentSessions = createMemo(() => {
|
||||
@@ -281,13 +259,11 @@ export default function Layout(props: ParentProps) {
|
||||
if (!directory) return
|
||||
const lastSession = store.lastSession[directory]
|
||||
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||
mobileSidebar.hide()
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
navigate(`/${params.dir}/session/${session?.id}`)
|
||||
mobileSidebar.hide()
|
||||
}
|
||||
|
||||
function openProject(directory: string, navigate = true) {
|
||||
@@ -321,20 +297,13 @@ export default function Layout(props: ParentProps) {
|
||||
createEffect(() => {
|
||||
if (!params.dir || !params.id) return
|
||||
const directory = base64Decode(params.dir)
|
||||
const id = params.id
|
||||
setStore("lastSession", directory, id)
|
||||
notification.session.markViewed(id)
|
||||
untrack(() => layout.projects.expand(directory))
|
||||
requestAnimationFrame(() => scrollToSession(id))
|
||||
setStore("lastSession", directory, params.id)
|
||||
notification.session.markViewed(params.id)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (isLargeViewport()) {
|
||||
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
|
||||
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
||||
} else {
|
||||
document.documentElement.style.setProperty("--dialog-left-margin", "0px")
|
||||
}
|
||||
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
|
||||
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
||||
})
|
||||
|
||||
function getDraggableId(event: unknown): string | undefined {
|
||||
@@ -448,9 +417,12 @@ export default function Layout(props: ParentProps) {
|
||||
session: Session
|
||||
slug: string
|
||||
project: LocalProject
|
||||
mobile?: boolean
|
||||
depth?: number
|
||||
childrenMap: Map<string, Session[]>
|
||||
}): JSX.Element => {
|
||||
const notification = useNotification()
|
||||
const depth = props.depth ?? 0
|
||||
const children = createMemo(() => props.childrenMap.get(props.session.id) ?? [])
|
||||
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
|
||||
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
@@ -465,20 +437,15 @@ export default function Layout(props: ParentProps) {
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full pr-2 py-1 rounded-md cursor-default transition-colors
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||
style={{ "padding-left": "16px" }}
|
||||
style={{ "padding-left": `${16 + depth * 12}px` }}
|
||||
>
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
||||
<Tooltip placement="right" value={props.session.title} gutter={10}>
|
||||
<A
|
||||
href={`${props.slug}/session/${props.session.id}`}
|
||||
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
||||
<span
|
||||
classList={{
|
||||
"text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
|
||||
"animate-pulse": isWorking(),
|
||||
}}
|
||||
>
|
||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||
{props.session.title}
|
||||
</span>
|
||||
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||
@@ -519,54 +486,66 @@ export default function Layout(props: ParentProps) {
|
||||
</A>
|
||||
</Tooltip>
|
||||
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
||||
<Tooltip
|
||||
placement={props.mobile ? "bottom" : "right"}
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Archive session</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("session.archive")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip placement="right" value="Archive session">
|
||||
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<For each={children()}>
|
||||
{(child) => (
|
||||
<SessionItem
|
||||
session={child}
|
||||
slug={props.slug}
|
||||
project={props.project}
|
||||
depth={depth + 1}
|
||||
childrenMap={props.childrenMap}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||
const SortableProject = (props: { project: LocalProject }): JSX.Element => {
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
||||
const sessions = createMemo(() => store.session.toSorted(sortSessions))
|
||||
const sessions = createMemo(() =>
|
||||
store.session.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)),
|
||||
)
|
||||
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
||||
const childSessionsByParent = createMemo(() => {
|
||||
const map = new Map<string, Session[]>()
|
||||
for (const session of sessions()) {
|
||||
if (session.parentID) {
|
||||
const children = map.get(session.parentID) ?? []
|
||||
children.push(session)
|
||||
map.set(session.parentID, children)
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
|
||||
const loadMoreSessions = async () => {
|
||||
setProjectStore("limit", (limit) => limit + 5)
|
||||
await globalSync.project.loadSessions(props.project.worktree)
|
||||
}
|
||||
const isExpanded = createMemo(() =>
|
||||
props.mobile ? mobileProjects.expanded(props.project.worktree) : props.project.expanded,
|
||||
)
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (props.mobile) {
|
||||
if (open) mobileProjects.expand(props.project.worktree)
|
||||
else mobileProjects.collapse(props.project.worktree)
|
||||
} else {
|
||||
if (open) layout.projects.expand(props.project.worktree)
|
||||
else layout.projects.collapse(props.project.worktree)
|
||||
}
|
||||
if (open) layout.projects.expand(props.project.worktree)
|
||||
else layout.projects.collapse(props.project.worktree)
|
||||
}
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<Switch>
|
||||
<Match when={showExpanded()}>
|
||||
<Collapsible variant="ghost" open={isExpanded()} class="gap-2 shrink-0" onOpenChange={handleOpenChange}>
|
||||
<Match when={layout.sidebar.opened()}>
|
||||
<Collapsible
|
||||
variant="ghost"
|
||||
open={props.project.expanded}
|
||||
class="gap-2 shrink-0"
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Button
|
||||
as={"div"}
|
||||
variant="ghost"
|
||||
@@ -577,7 +556,7 @@ export default function Layout(props: ParentProps) {
|
||||
project={props.project}
|
||||
class="group-hover/session:hidden"
|
||||
expandable
|
||||
notify={!isExpanded()}
|
||||
notify={!props.project.expanded}
|
||||
/>
|
||||
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
||||
</Collapsible.Trigger>
|
||||
@@ -592,15 +571,7 @@ export default function Layout(props: ParentProps) {
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>New session</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip placement="top" value="New session">
|
||||
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -609,7 +580,12 @@ export default function Layout(props: ParentProps) {
|
||||
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
|
||||
<For each={rootSessions()}>
|
||||
{(session) => (
|
||||
<SessionItem session={session} slug={slug()} project={props.project} mobile={props.mobile} />
|
||||
<SessionItem
|
||||
session={session}
|
||||
slug={slug()}
|
||||
project={props.project}
|
||||
childrenMap={childSessionsByParent()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={rootSessions().length === 0}>
|
||||
@@ -619,7 +595,7 @@ export default function Layout(props: ParentProps) {
|
||||
>
|
||||
<div class="flex items-center self-stretch w-full">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
|
||||
<Tooltip placement="right" value="New session">
|
||||
<A
|
||||
href={`${slug()}/session`}
|
||||
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
|
||||
@@ -674,23 +650,31 @@ export default function Layout(props: ParentProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
|
||||
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
|
||||
return (
|
||||
<>
|
||||
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
placement="right"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle sidebar</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
|
||||
</div>
|
||||
}
|
||||
inactive={expanded()}
|
||||
>
|
||||
return (
|
||||
<div class="relative flex-1 min-h-0 flex flex-col">
|
||||
<Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
|
||||
<div class="flex-1 min-h-0 flex">
|
||||
<div
|
||||
classList={{
|
||||
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
|
||||
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
|
||||
"border-r border-border-weak-base contain-strict": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
>
|
||||
<Show when={layout.sidebar.opened()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={150}
|
||||
max={window.innerWidth * 0.3}
|
||||
collapseThreshold={80}
|
||||
onResize={layout.sidebar.resize}
|
||||
onCollapse={layout.sidebar.close}
|
||||
/>
|
||||
</Show>
|
||||
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
||||
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
@@ -721,162 +705,101 @@ export default function Layout(props: ParentProps) {
|
||||
</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!sidebarProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
||||
<For each={layout.projects.list()}>
|
||||
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<ProjectDragOverlay />
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||
<Switch>
|
||||
<Match when={!providers.paid().length && expanded()}>
|
||||
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Getting started</div>
|
||||
<div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
|
||||
<div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
|
||||
>
|
||||
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
||||
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<ProjectDragOverlay />
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||
<Switch>
|
||||
<Match when={!providers.paid().length && layout.sidebar.opened()}>
|
||||
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Getting started</div>
|
||||
<div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
|
||||
<div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
|
||||
</div>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
|
||||
size="large"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
>
|
||||
<Show when={layout.sidebar.opened()}>Connect provider</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
>
|
||||
Connect provider
|
||||
<Show when={layout.sidebar.opened()}>Connect provider</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
icon="folder-add-left"
|
||||
onClick={chooseProject}
|
||||
>
|
||||
<Show when={expanded()}>Connect provider</Show>
|
||||
<Show when={layout.sidebar.opened()}>Open project</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Open project</span>
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
inactive={expanded()}
|
||||
>
|
||||
</Show>
|
||||
{/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
|
||||
{/* <Button */}
|
||||
{/* disabled */}
|
||||
{/* class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
|
||||
{/* variant="ghost" */}
|
||||
{/* size="large" */}
|
||||
{/* icon="settings-gear" */}
|
||||
{/* > */}
|
||||
{/* <Show when={layout.sidebar.opened()}>Settings</Show> */}
|
||||
{/* </Button> */}
|
||||
{/* </Tooltip> */}
|
||||
<Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
as={"a"}
|
||||
href="https://opencode.ai/desktop-feedback"
|
||||
target="_blank"
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="folder-add-left"
|
||||
onClick={chooseProject}
|
||||
icon="bubble-5"
|
||||
>
|
||||
<Show when={expanded()}>Open project</Show>
|
||||
<Show when={layout.sidebar.opened()}>Share feedback</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Tooltip placement="right" value="Share feedback" inactive={expanded()}>
|
||||
<Button
|
||||
as={"a"}
|
||||
href="https://opencode.ai/desktop-feedback"
|
||||
target="_blank"
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="bubble-5"
|
||||
>
|
||||
<Show when={expanded()}>Share feedback</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="relative flex-1 min-h-0 flex flex-col">
|
||||
<Header
|
||||
navigateToProject={navigateToProject}
|
||||
navigateToSession={navigateToSession}
|
||||
onMobileMenuToggle={mobileSidebar.toggle}
|
||||
/>
|
||||
<div class="flex-1 min-h-0 flex">
|
||||
<div
|
||||
classList={{
|
||||
"hidden xl:flex": true,
|
||||
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
|
||||
"flex-col gap-5.5 items-start self-stretch justify-between": true,
|
||||
"border-r border-border-weak-base contain-strict": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
>
|
||||
<Show when={layout.sidebar.opened()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={150}
|
||||
max={window.innerWidth * 0.3}
|
||||
collapseThreshold={80}
|
||||
onResize={layout.sidebar.resize}
|
||||
onCollapse={layout.sidebar.close}
|
||||
/>
|
||||
</Show>
|
||||
<SidebarContent />
|
||||
</div>
|
||||
<div class="xl:hidden">
|
||||
<div
|
||||
classList={{
|
||||
"fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
|
||||
"opacity-100 pointer-events-auto": mobileSidebar.open(),
|
||||
"opacity-0 pointer-events-none": !mobileSidebar.open(),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) mobileSidebar.hide()
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
classList={{
|
||||
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pt-12 pb-5 transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": mobileSidebar.open(),
|
||||
"-translate-x-full": !mobileSidebar.open(),
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SidebarContent mobile />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
|
||||
</div>
|
||||
<Toast.Region />
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
createRenderEffect,
|
||||
batch,
|
||||
} from "solid-js"
|
||||
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { createStore } from "solid-js/store"
|
||||
@@ -23,11 +22,11 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import {
|
||||
@@ -49,17 +48,13 @@ import { checksum } from "@opencode-ai/util/encode"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { StatusBar } from "@/components/status-bar"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
@@ -76,6 +71,7 @@ export default function Page() {
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
@@ -84,6 +80,7 @@ export default function Page() {
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
// Visible user messages excludes reverted messages (those >= revertMessageID)
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
@@ -91,24 +88,15 @@ export default function Page() {
|
||||
})
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
clickTimer: undefined as number | undefined,
|
||||
activeDraggable: undefined as string | undefined,
|
||||
activeTerminalDraggable: undefined as string | undefined,
|
||||
userInteracted: false,
|
||||
stepsExpanded: true,
|
||||
mobileStepsExpanded: {} as Record<string, boolean>,
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const [messageStore, setMessageStore] = createStore<{ messageId?: string }>({})
|
||||
const activeMessage = createMemo(() => {
|
||||
if (!store.messageId) return lastUserMessage()
|
||||
if (!messageStore.messageId) return lastUserMessage()
|
||||
// If the stored message is no longer visible (e.g., was reverted), fall back to last visible
|
||||
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
|
||||
const found = visibleUserMessages()?.find((m) => m.id === messageStore.messageId)
|
||||
return found ?? lastUserMessage()
|
||||
})
|
||||
const setActiveMessage = (message: UserMessage | undefined) => {
|
||||
setStore("messageId", message?.id)
|
||||
setMessageStore("messageId", message?.id)
|
||||
}
|
||||
|
||||
function navigateMessageByOffset(offset: number) {
|
||||
@@ -130,8 +118,34 @@ export default function Page() {
|
||||
setActiveMessage(msgs[targetIndex])
|
||||
}
|
||||
|
||||
const last = createMemo(
|
||||
() => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
|
||||
)
|
||||
const model = createMemo(() =>
|
||||
last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
|
||||
)
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
|
||||
const tokens = createMemo(() => {
|
||||
if (!last()) return
|
||||
const t = last().tokens
|
||||
return t.input + t.output + t.reasoning + t.cache.read + t.cache.write
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const total = tokens()
|
||||
const limit = model()?.limit.context
|
||||
if (!total || !limit) return 0
|
||||
return Math.round((total / limit) * 100)
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
clickTimer: undefined as number | undefined,
|
||||
activeDraggable: undefined as string | undefined,
|
||||
activeTerminalDraggable: undefined as string | undefined,
|
||||
userInteracted: false,
|
||||
stepsExpanded: true,
|
||||
})
|
||||
let inputRef!: HTMLDivElement
|
||||
|
||||
createEffect(() => {
|
||||
@@ -152,7 +166,7 @@ export default function Page() {
|
||||
() => visibleUserMessages().at(-1)?.id,
|
||||
(lastId, prevLastId) => {
|
||||
if (lastId && prevLastId && lastId > prevLastId) {
|
||||
setStore("messageId", undefined)
|
||||
setMessageStore("messageId", undefined)
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
@@ -225,14 +239,6 @@ export default function Page() {
|
||||
slash: "terminal",
|
||||
onSelect: () => layout.terminal.toggle(),
|
||||
},
|
||||
{
|
||||
id: "review.toggle",
|
||||
title: "Toggle review",
|
||||
description: "Show or hide the review panel",
|
||||
category: "View",
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => layout.review.toggle(),
|
||||
},
|
||||
{
|
||||
id: "terminal.new",
|
||||
title: "New terminal",
|
||||
@@ -278,15 +284,6 @@ export default function Page() {
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
},
|
||||
{
|
||||
id: "mcp.toggle",
|
||||
title: "Toggle MCPs",
|
||||
description: "Toggle MCPs",
|
||||
category: "MCP",
|
||||
keybind: "mod+;",
|
||||
slash: "mcp",
|
||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||
},
|
||||
{
|
||||
id: "agent.cycle",
|
||||
title: "Cycle agent",
|
||||
@@ -554,321 +551,279 @@ export default function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
const showTabs = createMemo(() => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0))
|
||||
|
||||
const mobileWorking = createMemo(() => status().type !== "idle")
|
||||
const mobileAutoScroll = createAutoScroll({
|
||||
working: mobileWorking,
|
||||
onUserInteracted: () => setStore("userInteracted", true),
|
||||
})
|
||||
|
||||
const MobileTurns = () => (
|
||||
<div
|
||||
ref={mobileAutoScroll.scrollRef}
|
||||
onScroll={mobileAutoScroll.handleScroll}
|
||||
onClick={mobileAutoScroll.handleInteraction}
|
||||
class="relative mt-2 min-w-0 w-full h-full overflow-y-auto no-scrollbar pb-12"
|
||||
>
|
||||
<div ref={mobileAutoScroll.contentRef} class="flex flex-col gap-45 items-start justify-start mt-4">
|
||||
<For each={visibleUserMessages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={message.id}
|
||||
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
||||
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const NewSessionView = () => (
|
||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
|
||||
<div class="text-20-medium text-text-weaker">New session</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
{getDirectory(sync.data.path.directory)}
|
||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="pencil-line" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
Last modified
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
const DesktopSessionContent = () => (
|
||||
<Switch>
|
||||
<Match when={params.id}>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={visibleUserMessages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={!showTabs()}
|
||||
/>
|
||||
<Show when={activeMessage()}>
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={activeMessage()!.id}
|
||||
stepsExpanded={store.stepsExpanded}
|
||||
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
classes={{
|
||||
root: "pb-20 flex-1 min-w-0",
|
||||
content: "pb-20",
|
||||
container:
|
||||
"w-full " +
|
||||
(!showTabs() ? "max-w-200 mx-auto px-6" : visibleUserMessages().length > 1 ? "pr-6 pl-18" : "px-6"),
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<NewSessionView />
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
<div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
|
||||
<Switch>
|
||||
<Match when={!params.id}>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<NewSessionView />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs class="flex-1 min-h-0 flex flex-col pb-28">
|
||||
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
|
||||
<div class="min-h-0 grow w-full">
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={tabs().active() ?? "chat"} onChange={tabs().open}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
|
||||
{diffs().length} Files Changed
|
||||
<Tabs.Trigger value="chat">
|
||||
<div class="flex gap-x-[17px] items-center">
|
||||
<div>Session</div>
|
||||
<Tooltip
|
||||
value={`${new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
}).format(tokens() ?? 0)} Tokens`}
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<ProgressCircle percentage={context() ?? 0} />
|
||||
<div class="text-14-regular text-text-weak text-left w-7">{context() ?? 0}%</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
<Show when={layout.review.state() === "tab" && diffs().length}>
|
||||
<Tabs.Trigger
|
||||
value="review"
|
||||
closeButton={
|
||||
<Tooltip value="Close tab" placement="bottom">
|
||||
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Show when={diffs()}>
|
||||
<DiffChanges changes={diffs()} variant="bars" />
|
||||
</Show>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>Review</div>
|
||||
<Show when={info()?.summary?.files}>
|
||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||
{info()?.summary?.files ?? 0}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={tabs().all() ?? []}>
|
||||
<For each={tabs().all() ?? []}>
|
||||
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
|
||||
<Tooltip value="Open file" class="flex items-center">
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="flex-1 !overflow-hidden">
|
||||
<MobileTurns />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content forceMount value="review" class="flex-1 !overflow-hidden hidden data-[selected]:block">
|
||||
<div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-32",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
</div>
|
||||
<Tabs.Content
|
||||
value="chat"
|
||||
class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden contain-strict"
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex-1 min-h-0": true,
|
||||
grid: layout.review.state() === "tab",
|
||||
flex: layout.review.state() === "pane",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
|
||||
"max-w-146 mx-auto": !wide(),
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={params.id}>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={visibleUserMessages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<Show when={activeMessage()}>
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={activeMessage()!.id}
|
||||
stepsExpanded={store.stepsExpanded}
|
||||
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
classes={{
|
||||
root: "pb-20 flex-1 min-w-0",
|
||||
content: "pb-20",
|
||||
container:
|
||||
"w-full " +
|
||||
(wide()
|
||||
? "max-w-200 mx-auto px-6"
|
||||
: visibleUserMessages().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
|
||||
<div class="text-20-medium text-text-weaker">New session</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
{getDirectory(sync.data.path.directory)}
|
||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="pencil-line" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
Last modified
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
||||
<div class="w-full max-w-200 px-6">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={layout.review.state() === "pane" && diffs().length}>
|
||||
<div
|
||||
classList={{
|
||||
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base contain-strict": true,
|
||||
}}
|
||||
>
|
||||
<SessionReview
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={diffs()}
|
||||
actions={
|
||||
<Tooltip value="Open in tab">
|
||||
<IconButton
|
||||
icon="expand"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
layout.review.tab()
|
||||
tabs().setActive("review")
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Show when={layout.review.state() === "tab" && diffs().length}>
|
||||
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
|
||||
<div
|
||||
classList={{
|
||||
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
|
||||
}}
|
||||
>
|
||||
<SessionReview
|
||||
classes={{
|
||||
root: "pb-40",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={diffs()}
|
||||
split
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<MobileTurns />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div class="absolute inset-x-0 bottom-4 flex flex-col justify-center items-center z-50 px-4">
|
||||
<div class="w-full">
|
||||
</Show>
|
||||
<For each={tabs().all()}>
|
||||
{(tab) => {
|
||||
const [file] = createResource(
|
||||
() => tab,
|
||||
async (tab) => {
|
||||
if (tab.startsWith("file://")) {
|
||||
return local.file.node(tab.replace("file://", ""))
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Tabs.Content value={tab} class="select-text mt-3">
|
||||
<Switch>
|
||||
<Match when={file()}>
|
||||
{(f) => (
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
file={{
|
||||
name: f().path,
|
||||
contents: f().content?.content ?? "",
|
||||
cacheKey: checksum(f().content?.content ?? ""),
|
||||
}}
|
||||
overflow="scroll"
|
||||
class="pb-40"
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
<Show when={store.activeDraggable}>
|
||||
{(draggedFile) => {
|
||||
const [file] = createResource(
|
||||
() => draggedFile(),
|
||||
async (tab) => {
|
||||
if (tab.startsWith("file://")) {
|
||||
return local.file.node(tab.replace("file://", ""))
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
return (
|
||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
||||
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
<Show when={tabs().active()}>
|
||||
<div class="absolute inset-x-0 px-6 max-w-200 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex min-h-0 grow w-full">
|
||||
<div
|
||||
class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
|
||||
style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
|
||||
>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<DesktopSessionContent />
|
||||
</div>
|
||||
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
||||
<div
|
||||
classList={{
|
||||
"w-full px-6": true,
|
||||
"max-w-200": !showTabs(),
|
||||
}}
|
||||
>
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={showTabs()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.session.width()}
|
||||
min={450}
|
||||
max={window.innerWidth * 0.45}
|
||||
onResize={layout.session.resize}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={showTabs()}>
|
||||
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={tabs().active() ?? "review"} onChange={tabs().open}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List>
|
||||
<Show when={diffs().length}>
|
||||
<Tabs.Trigger value="review">
|
||||
<div class="flex items-center gap-3">
|
||||
<Show when={diffs()}>
|
||||
<DiffChanges changes={diffs()} variant="bars" />
|
||||
</Show>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>Review</div>
|
||||
<Show when={info()?.summary?.files}>
|
||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||
{info()?.summary?.files ?? 0}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={tabs().all() ?? []}>
|
||||
<For each={tabs().all() ?? []}>
|
||||
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
|
||||
<Tooltip
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Open file</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("file.open")}</span>
|
||||
</div>
|
||||
}
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
<Show when={diffs().length}>
|
||||
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<SessionReview
|
||||
classes={{
|
||||
root: "pb-40",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={diffs()}
|
||||
split
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
<For each={tabs().all()}>
|
||||
{(tab) => {
|
||||
const [file] = createResource(
|
||||
() => tab,
|
||||
async (tab) => {
|
||||
if (tab.startsWith("file://")) {
|
||||
return local.file.node(tab.replace("file://", ""))
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Tabs.Content value={tab} class="select-text mt-3">
|
||||
<Switch>
|
||||
<Match when={file()}>
|
||||
{(f) => (
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
file={{
|
||||
name: f().path,
|
||||
contents: f().content?.content ?? "",
|
||||
cacheKey: checksum(f().content?.content ?? ""),
|
||||
}}
|
||||
overflow="scroll"
|
||||
class="pb-40"
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
<Show when={store.activeDraggable}>
|
||||
{(draggedFile) => {
|
||||
const [file] = createResource(
|
||||
() => draggedFile(),
|
||||
async (tab) => {
|
||||
if (tab.startsWith("file://")) {
|
||||
return local.file.node(tab.replace("file://", ""))
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
return (
|
||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
||||
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={layout.terminal.opened()}>
|
||||
<div
|
||||
class="hidden md:flex relative w-full flex-col shrink-0 border-t border-border-weak-base"
|
||||
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
||||
style={{ height: `${layout.terminal.height()}px` }}
|
||||
>
|
||||
<ResizeHandle
|
||||
@@ -894,15 +849,7 @@ export default function Page() {
|
||||
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<Tooltip
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>New terminal</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("terminal.new")}</span>
|
||||
</div>
|
||||
}
|
||||
class="flex items-center"
|
||||
>
|
||||
<Tooltip value="New Terminal" class="flex items-center">
|
||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -934,10 +881,6 @@ export default function Page() {
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</Show>
|
||||
<StatusBar>
|
||||
<SessionLspIndicator />
|
||||
<SessionMcpIndicator />
|
||||
</StatusBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user