mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 10:35:37 +08:00
Compare commits
16 Commits
v1.26.2
...
refactor/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fce8ad0893 | ||
|
|
cc7f652044 | ||
|
|
2207d05c1c | ||
|
|
a8dab0edcf | ||
|
|
e61f8a543d | ||
|
|
388134c7a5 | ||
|
|
ef51de259e | ||
|
|
1628868470 | ||
|
|
8f1042cf25 | ||
|
|
051a6b1e74 | ||
|
|
f1063fd339 | ||
|
|
27cd12432b | ||
|
|
004135ef01 | ||
|
|
b54cdf8168 | ||
|
|
42a131389a | ||
|
|
ebd1c0db92 |
50
.github/workflows/release.yml
vendored
Normal file
50
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
increment:
|
||||||
|
description: 'Version bump: patch, minor, major, or explicit (e.g. 1.27.0)'
|
||||||
|
required: true
|
||||||
|
default: 'patch'
|
||||||
|
type: string
|
||||||
|
release_name:
|
||||||
|
description: 'Custom release name (optional, defaults to "CloudCLI UI vX.Y.Z")'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.RELEASE_PAT }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
registry-url: https://registry.npmjs.org
|
||||||
|
|
||||||
|
- name: git config
|
||||||
|
run: |
|
||||||
|
git config user.name "${GITHUB_ACTOR}"
|
||||||
|
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
ARGS="--ci --increment=${{ inputs.increment }}"
|
||||||
|
if [ -n "${{ inputs.release_name }}" ]; then
|
||||||
|
ARGS="$ARGS --github.releaseName=\"${{ inputs.release_name }}\""
|
||||||
|
fi
|
||||||
|
npx release-it $ARGS
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"requireCleanWorkingDir": true
|
"requireCleanWorkingDir": true
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"publish": true
|
"publish": true,
|
||||||
|
"publishArgs": ["--access public"]
|
||||||
},
|
},
|
||||||
"github": {
|
"github": {
|
||||||
"release": true,
|
"release": true,
|
||||||
|
|||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -3,6 +3,32 @@
|
|||||||
All notable changes to CloudCLI UI will be documented in this file.
|
All notable changes to CloudCLI UI will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.28.0](https://github.com/siteboon/claudecodeui/compare/v1.27.1...v1.28.0) (2026-04-03)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
* adding session resume in the api ([8f1042c](https://github.com/siteboon/claudecodeui/commit/8f1042cf256be282f009adcceeb55ab2dddf3fba))
|
||||||
|
* moving new session button higher ([1628868](https://github.com/siteboon/claudecodeui/commit/16288684702dec894cf054291ca3d545ddb8214b))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* changing package name to @cloudcli-ai/cloudcli ([ef51de2](https://github.com/siteboon/claudecodeui/commit/ef51de259ea2b963bc15f058b084e11220bc216a))
|
||||||
|
|
||||||
|
## [1.27.1](https://github.com/siteboon/claudecodeui/compare/v1.26.3...v1.27.1) (2026-03-29)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* prevent split on undefined([#491](https://github.com/siteboon/claudecodeui/issues/491)) ([#563](https://github.com/siteboon/claudecodeui/issues/563)) ([b54cdf8](https://github.com/siteboon/claudecodeui/commit/b54cdf8168fc224e9907796e4229ae8ed34e6885))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* add release-it github action ([42a1313](https://github.com/siteboon/claudecodeui/commit/42a131389a6954df0d2c3bedd2cb6d3406c5ebc1))
|
||||||
|
* add terminal plugin in the plugins list ([004135e](https://github.com/siteboon/claudecodeui/commit/004135ef0187023e1da29c4a7137a28a42ebf9af))
|
||||||
|
* release tokens ([f1063fd](https://github.com/siteboon/claudecodeui/commit/f1063fd33964ccb517f5ebcdd14526ed162e1138))
|
||||||
|
* relicense to AGPL-3.0-or-later ([27cd124](https://github.com/siteboon/claudecodeui/commit/27cd12432b7d3237981f86acd9cc99532d843d4a))
|
||||||
|
|
||||||
|
## [1.26.3](https://github.com/siteboon/claudecodeui/compare/v1.26.2...v1.26.3) (2026-03-22)
|
||||||
|
|
||||||
## [1.26.2](https://github.com/siteboon/claudecodeui/compare/v1.26.0...v1.26.2) (2026-03-21)
|
## [1.26.2](https://github.com/siteboon/claudecodeui/compare/v1.26.0...v1.26.2) (2026-03-21)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -153,4 +153,4 @@ This automatically:
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under the [GPL-3.0 License](LICENSE).
|
By contributing, you agree that your contributions will be licensed under the [AGPL-3.0-or-later License](LICENSE), including the additional terms specified in Section 7 of the LICENSE file.
|
||||||
587
LICENSE
587
LICENSE
@@ -1,26 +1,21 @@
|
|||||||
# GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Version 3, 29 June 2007
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
Preamble
|
||||||
<https://fsf.org/>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
license document, but changing it is not allowed.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
## Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
to share and change all versions of a program--to make sure it remains
|
share and change all versions of a program--to make sure it remains free
|
||||||
free software for all its users. We, the Free Software Foundation, use
|
software for all its users.
|
||||||
the GNU General Public License for most of our software; it applies
|
|
||||||
also to any other work released this way by its authors. You can apply
|
|
||||||
it to your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -29,67 +24,55 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
have certain responsibilities if you distribute copies of the
|
you this License which gives you legal permission to copy, distribute
|
||||||
software, or if you modify it: responsibilities to respect the freedom
|
and/or modify the software.
|
||||||
of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the
|
|
||||||
manufacturer can do so. This is fundamentally incompatible with the
|
|
||||||
aim of protecting users' freedom to change the software. The
|
|
||||||
systematic pattern of such abuse occurs in the area of products for
|
|
||||||
individuals to use, which is precisely where it is most unacceptable.
|
|
||||||
Therefore, we have designed this version of the GPL to prohibit the
|
|
||||||
practice for those products. If such problems arise substantially in
|
|
||||||
other domains, we stand ready to extend this provision to those
|
|
||||||
domains in future versions of the GPL, as needed to protect the
|
|
||||||
freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish
|
|
||||||
to avoid the special danger that patents applied to a free program
|
|
||||||
could make it effectively proprietary. To prevent this, the GPL
|
|
||||||
assures that patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
|
||||||
## TERMS AND CONDITIONS
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
### 0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
of works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
in a fashion requiring copyright permission, other than the making of
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
an exact copy. The resulting work is called a "modified version" of
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
the earlier work or a work "based on" the earlier work.
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
A "covered work" means either the unmodified Program or a work based
|
||||||
on the Program.
|
on the Program.
|
||||||
@@ -102,12 +85,11 @@ distribution (with or without modification), making available to the
|
|||||||
public, and in some countries other activities as well.
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
To "convey" a work means any kind of propagation that enables other
|
||||||
parties to make or receive copies. Mere interaction with a user
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
through a computer network, with no transfer of a copy, is not
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices" to
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently visible
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
@@ -115,11 +97,11 @@ work under this License, and how to view a copy of this License. If
|
|||||||
the interface presents a list of user commands or options, such as a
|
the interface presents a list of user commands or options, such as a
|
||||||
menu, a prominent item in the list meets this criterion.
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
### 1. Source Code.
|
1. Source Code.
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work for
|
The "source code" for a work means the preferred form of the work
|
||||||
making modifications to it. "Object code" means any non-source form of
|
for making modifications to it. "Object code" means any non-source
|
||||||
a work.
|
form of a work.
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
A "Standard Interface" means an interface that either is an official
|
||||||
standard defined by a recognized standards body, or, in the case of
|
standard defined by a recognized standards body, or, in the case of
|
||||||
@@ -150,13 +132,14 @@ linked subprograms that the work is specifically designed to require,
|
|||||||
such as by intimate data communication or control flow between those
|
such as by intimate data communication or control flow between those
|
||||||
subprograms and other parts of the work.
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users can
|
The Corresponding Source need not include anything that users
|
||||||
regenerate automatically from other parts of the Corresponding Source.
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that same
|
The Corresponding Source for a work in source code form is that
|
||||||
work.
|
same work.
|
||||||
|
|
||||||
### 2. Basic Permissions.
|
2. Basic Permissions.
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
All rights granted under this License are granted for the term of
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
@@ -166,22 +149,22 @@ covered work is covered by this License only if the output, given its
|
|||||||
content, constitutes a covered work. This License acknowledges your
|
content, constitutes a covered work. This License acknowledges your
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not convey,
|
You may make, run and propagate covered works that you do not
|
||||||
without conditions so long as your license otherwise remains in force.
|
convey, without conditions so long as your license otherwise remains
|
||||||
You may convey covered works to others for the sole purpose of having
|
in force. You may convey covered works to others for the sole purpose
|
||||||
them make modifications exclusively for you, or provide you with
|
of having them make modifications exclusively for you, or provide you
|
||||||
facilities for running those works, provided that you comply with the
|
with facilities for running those works, provided that you comply with
|
||||||
terms of this License in conveying all material for which you do not
|
the terms of this License in conveying all material for which you do
|
||||||
control copyright. Those thus making or running the covered works for
|
not control copyright. Those thus making or running the covered works
|
||||||
you must do so exclusively on your behalf, under your direction and
|
for you must do so exclusively on your behalf, under your direction
|
||||||
control, on terms that prohibit them from making any copies of your
|
and control, on terms that prohibit them from making any copies of
|
||||||
copyrighted material outside their relationship with you.
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under the
|
Conveying under any other circumstances is permitted solely under
|
||||||
conditions stated below. Sublicensing is not allowed; section 10 makes
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
it unnecessary.
|
makes it unnecessary.
|
||||||
|
|
||||||
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
No covered work shall be deemed part of an effective technological
|
||||||
measure under any applicable law fulfilling obligations under article
|
measure under any applicable law fulfilling obligations under article
|
||||||
@@ -190,14 +173,14 @@ similar laws prohibiting or restricting circumvention of such
|
|||||||
measures.
|
measures.
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
circumvention of technological measures to the extent such
|
circumvention of technological measures to the extent such circumvention
|
||||||
circumvention is effected by exercising rights under this License with
|
is effected by exercising rights under this License with respect to
|
||||||
respect to the covered work, and you disclaim any intention to limit
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
operation or modification of the work as a means of enforcing, against
|
modification of the work as a means of enforcing, against the work's
|
||||||
the work's users, your or third parties' legal rights to forbid
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
circumvention of technological measures.
|
technological measures.
|
||||||
|
|
||||||
### 4. Conveying Verbatim Copies.
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
You may convey verbatim copies of the Program's source code as you
|
||||||
receive it, in any medium, provided that you conspicuously and
|
receive it, in any medium, provided that you conspicuously and
|
||||||
@@ -210,27 +193,29 @@ recipients a copy of this License along with the Program.
|
|||||||
You may charge any price or no price for each copy that you convey,
|
You may charge any price or no price for each copy that you convey,
|
||||||
and you may offer support or warranty protection for a fee.
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
### 5. Conveying Modified Source Versions.
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
You may convey a work based on the Program, or the modifications to
|
||||||
produce it from the Program, in the form of source code under the
|
produce it from the Program, in the form of source code under the
|
||||||
terms of section 4, provided that you also meet all of these
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
conditions:
|
|
||||||
|
|
||||||
- a) The work must carry prominent notices stating that you modified
|
a) The work must carry prominent notices stating that you modified
|
||||||
it, and giving a relevant date.
|
it, and giving a relevant date.
|
||||||
- b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under
|
b) The work must carry prominent notices stating that it is
|
||||||
section 7. This requirement modifies the requirement in section 4
|
released under this License and any conditions added under section
|
||||||
to "keep intact all notices".
|
7. This requirement modifies the requirement in section 4 to
|
||||||
- c) You must license the entire work, as a whole, under this
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
License to anyone who comes into possession of a copy. This
|
License to anyone who comes into possession of a copy. This
|
||||||
License will therefore apply, along with any applicable section 7
|
License will therefore apply, along with any applicable section 7
|
||||||
additional terms, to the whole of the work, and all its parts,
|
additional terms, to the whole of the work, and all its parts,
|
||||||
regardless of how they are packaged. This License gives no
|
regardless of how they are packaged. This License gives no
|
||||||
permission to license the work in any other way, but it does not
|
permission to license the work in any other way, but it does not
|
||||||
invalidate such permission if you have separately received it.
|
invalidate such permission if you have separately received it.
|
||||||
- d) If the work has interactive user interfaces, each must display
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
work need not make them do so.
|
work need not make them do so.
|
||||||
@@ -245,18 +230,19 @@ beyond what the individual works permit. Inclusion of a covered work
|
|||||||
in an aggregate does not cause this License to apply to the other
|
in an aggregate does not cause this License to apply to the other
|
||||||
parts of the aggregate.
|
parts of the aggregate.
|
||||||
|
|
||||||
### 6. Conveying Non-Source Forms.
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms of
|
You may convey a covered work in object code form under the terms
|
||||||
sections 4 and 5, provided that you also convey the machine-readable
|
of sections 4 and 5, provided that you also convey the
|
||||||
Corresponding Source under the terms of this License, in one of these
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
ways:
|
in one of these ways:
|
||||||
|
|
||||||
- a) Convey the object code in, or embodied in, a physical product
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
(including a physical distribution medium), accompanied by the
|
(including a physical distribution medium), accompanied by the
|
||||||
Corresponding Source fixed on a durable physical medium
|
Corresponding Source fixed on a durable physical medium
|
||||||
customarily used for software interchange.
|
customarily used for software interchange.
|
||||||
- b) Convey the object code in, or embodied in, a physical product
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
(including a physical distribution medium), accompanied by a
|
(including a physical distribution medium), accompanied by a
|
||||||
written offer, valid for at least three years and valid for as
|
written offer, valid for at least three years and valid for as
|
||||||
long as you offer spare parts or customer support for that product
|
long as you offer spare parts or customer support for that product
|
||||||
@@ -265,14 +251,16 @@ ways:
|
|||||||
product that is covered by this License, on a durable physical
|
product that is covered by this License, on a durable physical
|
||||||
medium customarily used for software interchange, for a price no
|
medium customarily used for software interchange, for a price no
|
||||||
more than your reasonable cost of physically performing this
|
more than your reasonable cost of physically performing this
|
||||||
conveying of source, or (2) access to copy the Corresponding
|
conveying of source, or (2) access to copy the
|
||||||
Source from a network server at no charge.
|
Corresponding Source from a network server at no charge.
|
||||||
- c) Convey individual copies of the object code with a copy of the
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
written offer to provide the Corresponding Source. This
|
written offer to provide the Corresponding Source. This
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
only if you received the object code with such an offer, in accord
|
only if you received the object code with such an offer, in accord
|
||||||
with subsection 6b.
|
with subsection 6b.
|
||||||
- d) Convey the object code by offering access from a designated
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
Corresponding Source in the same way through the same place at no
|
Corresponding Source in the same way through the same place at no
|
||||||
further charge. You need not require recipients to copy the
|
further charge. You need not require recipients to copy the
|
||||||
@@ -284,36 +272,36 @@ ways:
|
|||||||
Corresponding Source. Regardless of what server hosts the
|
Corresponding Source. Regardless of what server hosts the
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
available for as long as needed to satisfy these requirements.
|
available for as long as needed to satisfy these requirements.
|
||||||
- e) Convey the object code using peer-to-peer transmission,
|
|
||||||
provided you inform other peers where the object code and
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
Corresponding Source of the work are being offered to the general
|
you inform other peers where the object code and Corresponding
|
||||||
public at no charge under subsection 6d.
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
A separable portion of the object code, whose source code is excluded
|
||||||
from the Corresponding Source as a System Library, need not be
|
from the Corresponding Source as a System Library, need not be
|
||||||
included in conveying the object code work.
|
included in conveying the object code work.
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
tangible personal property which is normally used for personal,
|
tangible personal property which is normally used for personal, family,
|
||||||
family, or household purposes, or (2) anything designed or sold for
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
incorporation into a dwelling. In determining whether a product is a
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
consumer product, doubtful cases shall be resolved in favor of
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
coverage. For a particular product received by a particular user,
|
product received by a particular user, "normally used" refers to a
|
||||||
"normally used" refers to a typical or common use of that class of
|
typical or common use of that class of product, regardless of the status
|
||||||
product, regardless of the status of the particular user or of the way
|
of the particular user or of the way in which the particular user
|
||||||
in which the particular user actually uses, or expects or is expected
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
to use, the product. A product is a consumer product regardless of
|
is a consumer product regardless of whether the product has substantial
|
||||||
whether the product has substantial commercial, industrial or
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
non-consumer uses, unless such uses represent the only significant
|
the only significant mode of use of the product.
|
||||||
mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
"Installation Information" for a User Product means any methods,
|
||||||
procedures, authorization keys, or other information required to
|
procedures, authorization keys, or other information required to install
|
||||||
install and execute modified versions of a covered work in that User
|
and execute modified versions of a covered work in that User Product from
|
||||||
Product from a modified version of its Corresponding Source. The
|
a modified version of its Corresponding Source. The information must
|
||||||
information must suffice to ensure that the continued functioning of
|
suffice to ensure that the continued functioning of the modified object
|
||||||
the modified object code is in no case prevented or interfered with
|
code is in no case prevented or interfered with solely because
|
||||||
solely because modification has been made.
|
modification has been made.
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
If you convey an object code work under this section in, or with, or
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
@@ -327,13 +315,12 @@ modified object code on the User Product (for example, the work has
|
|||||||
been installed in ROM).
|
been installed in ROM).
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
The requirement to provide Installation Information does not include a
|
||||||
requirement to continue to provide support service, warranty, or
|
requirement to continue to provide support service, warranty, or updates
|
||||||
updates for a work that has been modified or installed by the
|
for a work that has been modified or installed by the recipient, or for
|
||||||
recipient, or for the User Product in which it has been modified or
|
the User Product in which it has been modified or installed. Access to a
|
||||||
installed. Access to a network may be denied when the modification
|
network may be denied when the modification itself materially and
|
||||||
itself materially and adversely affects the operation of the network
|
adversely affects the operation of the network or violates the rules and
|
||||||
or violates the rules and protocols for communication across the
|
protocols for communication across the network.
|
||||||
network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
in accord with this section must be in a format that is publicly
|
in accord with this section must be in a format that is publicly
|
||||||
@@ -341,7 +328,7 @@ documented (and with an implementation available to the public in
|
|||||||
source code form), and must require no special password or key for
|
source code form), and must require no special password or key for
|
||||||
unpacking, reading or copying.
|
unpacking, reading or copying.
|
||||||
|
|
||||||
### 7. Additional Terms.
|
7. Additional Terms.
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
License by making exceptions from one or more of its conditions.
|
License by making exceptions from one or more of its conditions.
|
||||||
@@ -360,26 +347,31 @@ additional permissions on material, added by you to a covered work,
|
|||||||
for which you have or can give appropriate copyright permission.
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
Notwithstanding any other provision of this License, for material you
|
||||||
add to a covered work, you may (if authorized by the copyright holders
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
of that material) supplement the terms of this License with terms:
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
- a) Disclaiming warranty or limiting liability differently from the
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
terms of sections 15 and 16 of this License; or
|
terms of sections 15 and 16 of this License; or
|
||||||
- b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
author attributions in that material or in the Appropriate Legal
|
author attributions in that material or in the Appropriate Legal
|
||||||
Notices displayed by works containing it; or
|
Notices displayed by works containing it; or
|
||||||
- c) Prohibiting misrepresentation of the origin of that material,
|
|
||||||
or requiring that modified versions of such material be marked in
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
reasonable ways as different from the original version; or
|
reasonable ways as different from the original version; or
|
||||||
- d) Limiting the use for publicity purposes of names of licensors
|
|
||||||
or authors of the material; or
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
- e) Declining to grant rights under trademark law for use of some
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
trade names, trademarks, or service marks; or
|
trade names, trademarks, or service marks; or
|
||||||
- f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions
|
f) Requiring indemnification of licensors and authors of that
|
||||||
of it) with contractual assumptions of liability to the recipient,
|
material by anyone who conveys the material (or modified versions of
|
||||||
for any liability that these contractual assumptions directly
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
impose on those licensors and authors.
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
All other non-permissive additional terms are considered "further
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
@@ -397,10 +389,10 @@ additional terms that apply to those files, or a notice indicating
|
|||||||
where to find the applicable terms.
|
where to find the applicable terms.
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
form of a separately written license, or stated as exceptions; the
|
form of a separately written license, or stated as exceptions;
|
||||||
above requirements apply either way.
|
the above requirements apply either way.
|
||||||
|
|
||||||
### 8. Termination.
|
8. Termination.
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
You may not propagate or modify a covered work except as expressly
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
@@ -408,12 +400,12 @@ modify it is void, and will automatically terminate your rights under
|
|||||||
this License (including any patent licenses granted under the third
|
this License (including any patent licenses granted under the third
|
||||||
paragraph of section 11).
|
paragraph of section 11).
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your license
|
However, if you cease all violation of this License, then your
|
||||||
from a particular copyright holder is reinstated (a) provisionally,
|
license from a particular copyright holder is reinstated (a)
|
||||||
unless and until the copyright holder explicitly and finally
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
terminates your license, and (b) permanently, if the copyright holder
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
fails to notify you of the violation by some reasonable means prior to
|
holder fails to notify you of the violation by some reasonable means
|
||||||
60 days after the cessation.
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
Moreover, your license from a particular copyright holder is
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
@@ -428,10 +420,10 @@ this License. If your rights have been terminated and not permanently
|
|||||||
reinstated, you do not qualify to receive new licenses for the same
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
material under section 10.
|
material under section 10.
|
||||||
|
|
||||||
### 9. Acceptance Not Required for Having Copies.
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or run
|
You are not required to accept this License in order to receive or
|
||||||
a copy of the Program. Ancillary propagation of a covered work
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
to receive a copy likewise does not require acceptance. However,
|
to receive a copy likewise does not require acceptance. However,
|
||||||
nothing other than this License grants you permission to propagate or
|
nothing other than this License grants you permission to propagate or
|
||||||
@@ -439,7 +431,7 @@ modify any covered work. These actions infringe copyright if you do
|
|||||||
not accept this License. Therefore, by modifying or propagating a
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
### 10. Automatic Licensing of Downstream Recipients.
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
Each time you convey a covered work, the recipient automatically
|
||||||
receives a license from the original licensors, to run, modify and
|
receives a license from the original licensors, to run, modify and
|
||||||
@@ -464,14 +456,14 @@ rights granted under this License, and you may not initiate litigation
|
|||||||
any patent claim is infringed by making, using, selling, offering for
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
sale, or importing the Program or any portion of it.
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
### 11. Patents.
|
11. Patents.
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
License of the Program or a work on which the Program is based. The
|
License of the Program or a work on which the Program is based. The
|
||||||
work thus licensed is called the contributor's "contributor version".
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims owned
|
A contributor's "essential patent claims" are all patent claims
|
||||||
or controlled by the contributor, whether already acquired or
|
owned or controlled by the contributor, whether already acquired or
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
by this License, of making, using, or selling its contributor version,
|
by this License, of making, using, or selling its contributor version,
|
||||||
but do not include claims that would be infringed only as a
|
but do not include claims that would be infringed only as a
|
||||||
@@ -514,100 +506,108 @@ or convey a specific copy of the covered work, then the patent license
|
|||||||
you grant is automatically extended to all recipients of the covered
|
you grant is automatically extended to all recipients of the covered
|
||||||
work and works based on it.
|
work and works based on it.
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within the
|
A patent license is "discriminatory" if it does not include within
|
||||||
scope of its coverage, prohibits the exercise of, or is conditioned on
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
the non-exercise of one or more of the rights that are specifically
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
granted under this License. You may not convey a covered work if you
|
specifically granted under this License. You may not convey a covered
|
||||||
are a party to an arrangement with a third party that is in the
|
work if you are a party to an arrangement with a third party that is
|
||||||
business of distributing software, under which you make payment to the
|
in the business of distributing software, under which you make payment
|
||||||
third party based on the extent of your activity of conveying the
|
to the third party based on the extent of your activity of conveying
|
||||||
work, and under which the third party grants, to any of the parties
|
the work, and under which the third party grants, to any of the
|
||||||
who would receive the covered work from you, a discriminatory patent
|
parties who would receive the covered work from you, a discriminatory
|
||||||
license (a) in connection with copies of the covered work conveyed by
|
patent license (a) in connection with copies of the covered work
|
||||||
you (or copies made from those copies), or (b) primarily for and in
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
connection with specific products or compilations that contain the
|
for and in connection with specific products or compilations that
|
||||||
covered work, unless you entered into that arrangement, or that patent
|
contain the covered work, unless you entered into that arrangement,
|
||||||
license was granted, prior to 28 March 2007.
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
any implied license or other defenses to infringement that may
|
any implied license or other defenses to infringement that may
|
||||||
otherwise be available to you under applicable patent law.
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
### 12. No Surrender of Others' Freedom.
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
covered work so as to satisfy simultaneously your obligations under
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
this License and any other pertinent obligations, then as a
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
consequence you may not convey it at all. For example, if you agree to
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
terms that obligate you to collect a royalty for further conveying
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
from those to whom you convey the Program, the only way you could
|
the Program, the only way you could satisfy both those terms and this
|
||||||
satisfy both those terms and this License would be to refrain entirely
|
License would be to refrain entirely from conveying the Program.
|
||||||
from conveying the Program.
|
|
||||||
|
|
||||||
### 13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
### 14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
of the GNU General Public License from time to time. Such new versions
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
will be similar in spirit to the present version, but may differ in
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
detail to address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
Each version is given a distinguishing version number. If the
|
||||||
specifies that a certain numbered version of the GNU General Public
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
License "or any later version" applies to it, you have the option of
|
Public License "or any later version" applies to it, you have the
|
||||||
following the terms and conditions either of that numbered version or
|
option of following the terms and conditions either of that numbered
|
||||||
of any later version published by the Free Software Foundation. If the
|
version or of any later version published by the Free Software
|
||||||
Program does not specify a version number of the GNU General Public
|
Foundation. If the Program does not specify a version number of the
|
||||||
License, you may choose any version ever published by the Free
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future versions
|
If the Program specifies that a proxy can decide which future
|
||||||
of the GNU General Public License can be used, that proxy's public
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
statement of acceptance of a version permanently authorizes you to
|
public statement of acceptance of a version permanently authorizes you
|
||||||
choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
Later license versions may give you additional or different
|
||||||
permissions. However, no additional obligations are imposed on any
|
permissions. However, no additional obligations are imposed on any
|
||||||
author or copyright holder as a result of your choosing to follow a
|
author or copyright holder as a result of your choosing to follow a
|
||||||
later version.
|
later version.
|
||||||
|
|
||||||
### 15. Disclaimer of Warranty.
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
CORRECTION.
|
|
||||||
|
|
||||||
### 16. Limitation of Liability.
|
16. Limitation of Liability.
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
SUCH DAMAGES.
|
||||||
|
|
||||||
### 17. Interpretation of Sections 15 and 16.
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
above cannot be given local legal effect according to their terms,
|
above cannot be given local legal effect according to their terms,
|
||||||
@@ -618,58 +618,101 @@ copy of the Program in return for a fee.
|
|||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
## How to Apply These Terms to Your New Programs
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
If you develop a new program, and you want it to be of the greatest
|
||||||
possible use to the public, the best way to achieve this is to make it
|
possible use to the public, the best way to achieve this is to make it
|
||||||
free software which everyone can redistribute and change under these
|
free software which everyone can redistribute and change under these terms.
|
||||||
terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to
|
To do so, attach the following notices to the program. It is safest
|
||||||
attach them to the start of each source file to most effectively state
|
to attach them to the start of each source file to most effectively
|
||||||
the exclusion of warranty; and each file should have at least the
|
state the exclusion of warranty; and each file should have at least
|
||||||
"copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
This is free software, and you are welcome to redistribute it
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
under certain conditions; type `show c' for details.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The hypothetical commands \`show w' and \`show c' should show the
|
=========================================================================
|
||||||
appropriate parts of the General Public License. Of course, your
|
|
||||||
program's commands might be different; for a GUI interface, you would
|
|
||||||
use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or
|
ADDITIONAL TERMS pursuant to Section 7 of the GNU Affero General Public
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
License, Version 3
|
||||||
necessary. For more information on this, and how to apply and follow
|
|
||||||
the GNU GPL, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your
|
The following additional terms apply to the CloudCLI UI project
|
||||||
program into proprietary programs. If your program is a subroutine
|
(https://github.com/siteboon/claudecodeui). These terms are authorized
|
||||||
library, you may consider it more useful to permit linking proprietary
|
by Siteboon AI B.V. as copyright holder pursuant to Section 7 of the AGPL-3.0.
|
||||||
applications with the library. If this is what you want to do, use the
|
|
||||||
GNU Lesser General Public License instead of this License. But first,
|
1. Attribution Requirement (Section 7(b))
|
||||||
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
All copies, modified versions, and derivative works of this software
|
||||||
|
must preserve the following attribution notice in their documentation,
|
||||||
|
README, or Appropriate Legal Notices:
|
||||||
|
|
||||||
|
"CloudCLI UI (https://github.com/siteboon/claudecodeui)"
|
||||||
|
|
||||||
|
This notice must be reasonably prominent and not hidden in a manner
|
||||||
|
designed to avoid notice by recipients of the software.
|
||||||
|
|
||||||
|
2. Prohibition of Misrepresentation (Section 7(c))
|
||||||
|
|
||||||
|
You may not misrepresent the origin of this software. Modified
|
||||||
|
versions of the software must be clearly and prominently marked as
|
||||||
|
such, and must not be presented as the original CloudCLI UI software.
|
||||||
|
|
||||||
|
3. Limitation on Publicity (Section 7(d))
|
||||||
|
|
||||||
|
The names "Siteboon" and "CloudCLI" may not be used for publicity
|
||||||
|
purposes to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission from Siteboon AI B.V.
|
||||||
|
|
||||||
|
4. No Trademark Rights (Section 7(e))
|
||||||
|
|
||||||
|
This License does not grant permission to use the trade names,
|
||||||
|
trademarks, service marks, or product names "CloudCLI," "CloudCLI UI,"
|
||||||
|
or "Siteboon," except as required for reasonable and customary use in
|
||||||
|
describing the origin of the work and reproducing the content of the
|
||||||
|
attribution notice required above.
|
||||||
|
|
||||||
|
=========================================================================
|
||||||
|
|
||||||
|
RELICENSING NOTICE
|
||||||
|
|
||||||
|
Contributions made by Siteboon AI B.V. prior to commit
|
||||||
|
004135ef0187023e1da29c4a7137a28a42ebf9af (2026-03-28) were originally
|
||||||
|
published under GPL-3.0. These contributions are hereby relicensed under
|
||||||
|
AGPL-3.0-or-later by Siteboon AI B.V., as copyright holder.
|
||||||
|
|
||||||
|
Contributions made by other authors prior to the above commit remain
|
||||||
|
under GPL-3.0 and are incorporated into this AGPL-3.0-or-later work as
|
||||||
|
permitted by GPL-3.0 Section 13 ("Use with the GNU Affero General Public
|
||||||
|
License").
|
||||||
|
|
||||||
|
All new contributions from the above commit onward are licensed under
|
||||||
|
AGPL-3.0-or-later.
|
||||||
|
|||||||
13
NOTICE
Normal file
13
NOTICE
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CloudCLI UI
|
||||||
|
Copyright 2025-2026 Siteboon AI B.V. and contributors
|
||||||
|
|
||||||
|
This software is licensed under the GNU Affero General Public License v3.0
|
||||||
|
or later (AGPL-3.0-or-later). See the LICENSE file for the full license text,
|
||||||
|
including additional terms under Section 7.
|
||||||
|
|
||||||
|
Originally developed by Siteboon AI B.V. (https://github.com/siteboon/claudecodeui).
|
||||||
|
|
||||||
|
Contributions by Siteboon AI B.V. prior to commit 004135ef were originally
|
||||||
|
published under GPL-3.0 and are hereby relicensed to AGPL-3.0-or-later.
|
||||||
|
Contributions by other authors prior to that commit remain under GPL-3.0
|
||||||
|
and are incorporated into this work as permitted by GPL-3.0 Section 13.
|
||||||
@@ -79,13 +79,13 @@ Der schnellste Einstieg – keine lokale Einrichtung erforderlich. Erhalte eine
|
|||||||
CloudCLI UI sofort mit **npx** ausprobieren (erfordert **Node.js** v22+):
|
CloudCLI UI sofort mit **npx** ausprobieren (erfordert **Node.js** v22+):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @siteboon/claude-code-ui
|
npx @cloudcli-ai/cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
Oder **global** installieren für regelmäßige Nutzung:
|
Oder **global** installieren für regelmäßige Nutzung:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @siteboon/claude-code-ui
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
cloudcli
|
cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ CloudCLI UI ist die Open-Source-UI-Schicht, die CloudCLI Cloud antreibt. Du kann
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Am besten für** | Entwickler:innen, die eine vollständige UI für lokale Agent-Sitzungen auf ihrem eigenen Rechner möchten | Teams und Entwickler:innen, die Agents in der Cloud betreiben möchten, überall erreichbar |
|
| **Am besten für** | Entwickler:innen, die eine vollständige UI für lokale Agent-Sitzungen auf ihrem eigenen Rechner möchten | Teams und Entwickler:innen, die Agents in der Cloud betreiben möchten, überall erreichbar |
|
||||||
| **Zugriff** | Browser via `[deineIP]:port` | Browser, jede IDE, REST API, n8n |
|
| **Zugriff** | Browser via `[deineIP]:port` | Browser, jede IDE, REST API, n8n |
|
||||||
| **Einrichtung** | `npx @siteboon/claude-code-ui` | Keine Einrichtung erforderlich |
|
| **Einrichtung** | `npx @cloudcli-ai/cloudcli` | Keine Einrichtung erforderlich |
|
||||||
| **Rechner muss laufen** | Ja | Nein |
|
| **Rechner muss laufen** | Ja | Nein |
|
||||||
| **Mobiler Zugriff** | Jeder Browser im Netzwerk | Jedes Gerät, native App in Entwicklung |
|
| **Mobiler Zugriff** | Jeder Browser im Netzwerk | Jedes Gerät, native App in Entwicklung |
|
||||||
| **Verfügbare Sitzungen** | Alle Sitzungen automatisch aus `~/.claude` erkannt | Alle Sitzungen in deiner Cloud-Umgebung |
|
| **Verfügbare Sitzungen** | Alle Sitzungen automatisch aus `~/.claude` erkannt | Alle Sitzungen in deiner Cloud-Umgebung |
|
||||||
|
|||||||
@@ -75,13 +75,13 @@
|
|||||||
**npx** で今すぐ CloudCLI UI を試せます(**Node.js** v22+ が必要):
|
**npx** で今すぐ CloudCLI UI を試せます(**Node.js** v22+ が必要):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @siteboon/claude-code-ui
|
npx @cloudcli-ai/cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
または、普段使いするなら **グローバル** にインストール:
|
または、普段使いするなら **グローバル** にインストール:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @siteboon/claude-code-ui
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
cloudcli
|
cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ CloudCLI UI は、CloudCLI Cloud を支えるオープンソースの UI レイ
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **対象ユーザー** | 自分のマシン上でローカルの agent セッションに対してフル UI を使いたい開発者 | クラウド上で動く agents をどこからでも利用したいチーム/開発者 |
|
| **対象ユーザー** | 自分のマシン上でローカルの agent セッションに対してフル UI を使いたい開発者 | クラウド上で動く agents をどこからでも利用したいチーム/開発者 |
|
||||||
| **アクセス方法** | ブラウザ(`[yourip]:port`) | ブラウザ、任意の IDE、REST API、n8n |
|
| **アクセス方法** | ブラウザ(`[yourip]:port`) | ブラウザ、任意の IDE、REST API、n8n |
|
||||||
| **セットアップ** | `npx @siteboon/claude-code-ui` | セットアップ不要 |
|
| **セットアップ** | `npx @cloudcli-ai/cloudcli` | セットアップ不要 |
|
||||||
| **マシンの稼働継続** | はい | いいえ |
|
| **マシンの稼働継続** | はい | いいえ |
|
||||||
| **モバイルアクセス** | 同一ネットワーク内の任意のブラウザ | 任意のデバイス(ネイティブアプリも準備中) |
|
| **モバイルアクセス** | 同一ネットワーク内の任意のブラウザ | 任意のデバイス(ネイティブアプリも準備中) |
|
||||||
| **利用可能なセッション** | `~/.claude` から全セッションを自動検出 | クラウド環境内の全セッション |
|
| **利用可能なセッション** | `~/.claude` から全セッションを自動検出 | クラウド環境内の全セッション |
|
||||||
|
|||||||
@@ -75,13 +75,13 @@
|
|||||||
**npx**로 즉시 CloudCLI UI를 실행하세요 (Node.js v22+ 필요):
|
**npx**로 즉시 CloudCLI UI를 실행하세요 (Node.js v22+ 필요):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @siteboon/claude-code-ui
|
npx @cloudcli-ai/cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
**정기적으로 사용한다면 전역 설치:**
|
**정기적으로 사용한다면 전역 설치:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @siteboon/claude-code-ui
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
cloudcli
|
cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ CloudCLI UI는 CloudCLI Cloud를 구동하는 오픈 소스 UI 계층입니다.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **적합한 대상** | 로컬 에이전트 세션을 위한 전체 UI가 필요한 개발자 | 어디서든 접근 가능한 클라우드에서 에이전트를 운영하고자 하는 팀 및 개발자 |
|
| **적합한 대상** | 로컬 에이전트 세션을 위한 전체 UI가 필요한 개발자 | 어디서든 접근 가능한 클라우드에서 에이전트를 운영하고자 하는 팀 및 개발자 |
|
||||||
| **접근 방법** | `[yourip]:port`를 통해 브라우저 접속 | 브라우저, IDE, REST API, n8n |
|
| **접근 방법** | `[yourip]:port`를 통해 브라우저 접속 | 브라우저, IDE, REST API, n8n |
|
||||||
| **설정** | `npx @siteboon/claude-code-ui` | 설정 불필요 |
|
| **설정** | `npx @cloudcli-ai/cloudcli` | 설정 불필요 |
|
||||||
| **기기 유지 필요 여부** | 예 (머신 켜둬야 함) | 아니오 |
|
| **기기 유지 필요 여부** | 예 (머신 켜둬야 함) | 아니오 |
|
||||||
| **모바일 접근** | 네트워크 내 브라우저 | 모든 기기 (네이티브 앱 예정) |
|
| **모바일 접근** | 네트워크 내 브라우저 | 모든 기기 (네이티브 앱 예정) |
|
||||||
| **세션 접근** | `~/.claude`에서 자동 발견 | 클라우드 환경 내 세션 |
|
| **세션 접근** | `~/.claude`에서 자동 발견 | 클라우드 환경 내 세션 |
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -79,13 +79,13 @@ The fastest way to get started — no local setup required. Get a fully managed,
|
|||||||
Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+):
|
Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+):
|
||||||
|
|
||||||
```
|
```
|
||||||
npx @siteboon/claude-code-ui
|
npx @cloudcli-ai/cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install **globally** for regular use:
|
Or install **globally** for regular use:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install -g @siteboon/claude-code-ui
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
cloudcli
|
cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Best for** | Developers who want a full UI for local agent sessions on their own machine | Teams and developers who want agents running in the cloud, accessible from anywhere |
|
| **Best for** | Developers who want a full UI for local agent sessions on their own machine | Teams and developers who want agents running in the cloud, accessible from anywhere |
|
||||||
| **How you access it** | Browser via `[yourip]:port` | Browser, any IDE, REST API, n8n |
|
| **How you access it** | Browser via `[yourip]:port` | Browser, any IDE, REST API, n8n |
|
||||||
| **Setup** | `npx @siteboon/claude-code-ui` | No setup required |
|
| **Setup** | `npx @cloudcli-ai/cloudcli` | No setup required |
|
||||||
| **Machine needs to stay on** | Yes | No |
|
| **Machine needs to stay on** | Yes | No |
|
||||||
| **Mobile access** | Any browser on your network | Any device, native app coming |
|
| **Mobile access** | Any browser on your network | Any device, native app coming |
|
||||||
| **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment |
|
| **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment |
|
||||||
@@ -213,9 +213,11 @@ Yes, for self-hosted. CloudCLI UI reads from and writes to the same `~/.claude`
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GNU General Public License v3.0 - see [LICENSE](LICENSE) file for details.
|
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see [LICENSE](LICENSE) for the full text, including additional terms under Section 7.
|
||||||
|
|
||||||
This project is open source and free to use, modify, and distribute under the GPL v3 license.
|
This project is open source and free to use, modify, and distribute under the AGPL-3.0-or-later license. If you modify this software and run it as a network service, you must make your modified source code available to users of that service.
|
||||||
|
|
||||||
|
CloudCLI UI - (https://cloudcli.ai).
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,13 @@
|
|||||||
Попробовать CloudCLI UI можно сразу через **npx** (требуется **Node.js** v22+):
|
Попробовать CloudCLI UI можно сразу через **npx** (требуется **Node.js** v22+):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @siteboon/claude-code-ui
|
npx @cloudcli-ai/cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
Или установить **глобально** для регулярного использования:
|
Или установить **глобально** для регулярного использования:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @siteboon/claude-code-ui
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
cloudcli
|
cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ CloudCLI UI — это open source UI-слой, на котором постро
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Лучше всего подходит для** | Разработчиков, которым нужен полноценный UI для локальных агентских сессий на своей машине | Команд и разработчиков, которым нужны агенты в облаке с доступом откуда угодно |
|
| **Лучше всего подходит для** | Разработчиков, которым нужен полноценный UI для локальных агентских сессий на своей машине | Команд и разработчиков, которым нужны агенты в облаке с доступом откуда угодно |
|
||||||
| **Как вы получаете доступ** | Браузер через `[yourip]:port` | Браузер, любая IDE, REST API, n8n |
|
| **Как вы получаете доступ** | Браузер через `[yourip]:port` | Браузер, любая IDE, REST API, n8n |
|
||||||
| **Настройка** | `npx @siteboon/claude-code-ui` | Настройка не требуется |
|
| **Настройка** | `npx @cloudcli-ai/cloudcli` | Настройка не требуется |
|
||||||
| **Машина должна оставаться включённой** | Да | Нет |
|
| **Машина должна оставаться включённой** | Да | Нет |
|
||||||
| **Доступ с мобильных устройств** | Любой браузер в вашей сети | Любое устройство, нативное приложение в разработке |
|
| **Доступ с мобильных устройств** | Любой браузер в вашей сети | Любое устройство, нативное приложение в разработке |
|
||||||
| **Доступные сессии** | Все сессии автоматически обнаруживаются из `~/.claude` | Все сессии внутри вашей облачной среды |
|
| **Доступные сессии** | Все сессии автоматически обнаруживаются из `~/.claude` | Все сессии внутри вашей облачной среды |
|
||||||
|
|||||||
@@ -75,13 +75,13 @@
|
|||||||
启动 CloudCLI UI,只需一行 `npx`(需要 Node.js v22+):
|
启动 CloudCLI UI,只需一行 `npx`(需要 Node.js v22+):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @siteboon/claude-code-ui
|
npx @cloudcli-ai/cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
或进行全局安装,便于日常使用:
|
或进行全局安装,便于日常使用:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @siteboon/claude-code-ui
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
cloudcli
|
cloudcli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ CloudCLI UI 是 CloudCLI Cloud 的开源 UI 层。你可以在本地机器上自
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **适合对象** | 需要为本地代理会话提供完整 UI 的开发者 | 需要部署在云端,随时从任何地方访问代理的团队与开发者 |
|
| **适合对象** | 需要为本地代理会话提供完整 UI 的开发者 | 需要部署在云端,随时从任何地方访问代理的团队与开发者 |
|
||||||
| **访问方式** | 通过 `[yourip]:port` 在浏览器中访问 | 浏览器、任意 IDE、REST API、n8n |
|
| **访问方式** | 通过 `[yourip]:port` 在浏览器中访问 | 浏览器、任意 IDE、REST API、n8n |
|
||||||
| **设置** | `npx @siteboon/claude-code-ui` | 无需设置 |
|
| **设置** | `npx @cloudcli-ai/cloudcli` | 无需设置 |
|
||||||
| **机器需保持开机吗** | 是 | 否 |
|
| **机器需保持开机吗** | 是 | 否 |
|
||||||
| **移动端访问** | 网络内任意浏览器 | 任意设备(原生应用即将推出) |
|
| **移动端访问** | 网络内任意浏览器 | 任意设备(原生应用即将推出) |
|
||||||
| **可用会话** | 自动发现 `~/.claude` 中的所有会话 | 云端环境内的会话 |
|
| **可用会话** | 自动发现 `~/.claude` 中的所有会话 | 云端环境内的会话 |
|
||||||
|
|||||||
89
docker/README.md
Normal file
89
docker/README.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# CloudCLI — Docker Sandbox Templates
|
||||||
|
|
||||||
|
Run AI coding agents with a full web IDE inside [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/).
|
||||||
|
|
||||||
|
Instead of a terminal-only experience, get a browser-based interface with chat, file explorer, git panel, shell, and MCP configuration — all running safely inside an isolated sandbox.
|
||||||
|
|
||||||
|
## Available Templates
|
||||||
|
|
||||||
|
| Template | Base Image | Agent |
|
||||||
|
|----------|-----------|-------|
|
||||||
|
| `cloudcli-ai/sandbox:claude-code` | `docker/sandbox-templates:claude-code` | Claude Code |
|
||||||
|
| `cloudcli-ai/sandbox:codex` | `docker/sandbox-templates:codex` | OpenAI Codex |
|
||||||
|
| `cloudcli-ai/sandbox:gemini` | `docker/sandbox-templates:gemini` | Gemini CLI |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Start a sandbox with the template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sbx run --template docker.io/cloudcli-ai/sandbox:claude-code claude ~/my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Forward the UI port
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sbx ports <sandbox-name> --publish 3001:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Open the browser
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
On first visit you'll set a password — this protects the UI if the port is ever exposed beyond localhost.
|
||||||
|
|
||||||
|
## What You Get
|
||||||
|
|
||||||
|
- **Chat** — Rich conversation UI with markdown rendering, code blocks, and message history
|
||||||
|
- **Files** — Visual file tree with syntax-highlighted editor
|
||||||
|
- **Git** — Diff viewer, staging, branch switching, and commit — all visual
|
||||||
|
- **Shell** — Built-in terminal emulator
|
||||||
|
- **MCP** — Configure Model Context Protocol servers through the UI
|
||||||
|
- **Mobile** — Works on tablet and phone browsers
|
||||||
|
|
||||||
|
## Building Locally
|
||||||
|
|
||||||
|
All Dockerfiles share scripts from `shared/`. Build with the `docker/` directory as context:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Claude Code variant
|
||||||
|
docker build -f docker/claude-code/Dockerfile -t cloudcli-sandbox:claude-code docker/
|
||||||
|
|
||||||
|
# Codex variant
|
||||||
|
docker build -f docker/codex/Dockerfile -t cloudcli-sandbox:codex docker/
|
||||||
|
|
||||||
|
# Gemini variant
|
||||||
|
docker build -f docker/gemini/Dockerfile -t cloudcli-sandbox:gemini docker/
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Each template extends Docker's official sandbox base image and adds:
|
||||||
|
|
||||||
|
1. **Node.js 22** — Runtime for CloudCLI
|
||||||
|
2. **CloudCLI** — Installed globally via `npm install -g @cloudcli-ai/cloudcli`
|
||||||
|
3. **Auto-start** — The UI server starts in the background when the sandbox shell opens (port 3001)
|
||||||
|
|
||||||
|
The agent (Claude Code, Codex, or Gemini) comes from the base image. CloudCLI connects to it and provides the web interface on top.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Environment Variable | Default | Description |
|
||||||
|
|---------------------|---------|-------------|
|
||||||
|
| `SERVER_PORT` | `3001` | Port for the web UI |
|
||||||
|
| `HOST` | `0.0.0.0` | Bind address |
|
||||||
|
| `DATABASE_PATH` | `~/.cloudcli/auth.db` | SQLite database location |
|
||||||
|
|
||||||
|
## Network Policies
|
||||||
|
|
||||||
|
If your sandbox uses restricted network policies, allow the UI port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sbx policy allow network "localhost:3001"
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
These templates are free and open-source under the same license as CloudCLI (AGPL-3.0-or-later).
|
||||||
11
docker/claude-code/Dockerfile
Normal file
11
docker/claude-code/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM docker/sandbox-templates:claude-code
|
||||||
|
|
||||||
|
USER root
|
||||||
|
COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
|
||||||
|
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
|
||||||
|
|
||||||
|
USER agent
|
||||||
|
RUN npm install -g @cloudcli-ai/cloudcli
|
||||||
|
|
||||||
|
COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
|
||||||
|
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc
|
||||||
11
docker/codex/Dockerfile
Normal file
11
docker/codex/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM docker/sandbox-templates:codex
|
||||||
|
|
||||||
|
USER root
|
||||||
|
COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
|
||||||
|
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
|
||||||
|
|
||||||
|
USER agent
|
||||||
|
RUN npm install -g @cloudcli-ai/cloudcli
|
||||||
|
|
||||||
|
COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
|
||||||
|
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc
|
||||||
11
docker/gemini/Dockerfile
Normal file
11
docker/gemini/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM docker/sandbox-templates:gemini
|
||||||
|
|
||||||
|
USER root
|
||||||
|
COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
|
||||||
|
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
|
||||||
|
|
||||||
|
USER agent
|
||||||
|
RUN npm install -g @cloudcli-ai/cloudcli
|
||||||
|
|
||||||
|
COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
|
||||||
|
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc
|
||||||
14
docker/shared/install-cloudcli.sh
Normal file
14
docker/shared/install-cloudcli.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install Node.js 22 LTS
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
|
||||||
|
# Install Node.js + build tools needed for native modules (node-pty, better-sqlite3, bcrypt)
|
||||||
|
# Node.js + build tools for native modules + common dev tools
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
nodejs build-essential python3 python3-setuptools \
|
||||||
|
jq ripgrep sqlite3 zip unzip tree vim-tiny
|
||||||
|
|
||||||
|
# Clean up apt cache to reduce image size
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
23
docker/shared/start-cloudcli.sh
Normal file
23
docker/shared/start-cloudcli.sh
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Auto-start CloudCLI server in background if not already running.
|
||||||
|
# This script is sourced from ~/.bashrc on sandbox shell open.
|
||||||
|
|
||||||
|
if ! pgrep -f "server/index.js" > /dev/null 2>&1; then
|
||||||
|
# Start the pre-installed version immediately
|
||||||
|
nohup cloudcli start --port 3001 > /tmp/cloudcli-ui.log 2>&1 &
|
||||||
|
disown
|
||||||
|
|
||||||
|
# Check for updates in the background (non-blocking)
|
||||||
|
nohup npm update -g @cloudcli-ai/cloudcli > /tmp/cloudcli-update.log 2>&1 &
|
||||||
|
disown
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " CloudCLI is starting on port 3001..."
|
||||||
|
echo ""
|
||||||
|
echo " To access the web UI, forward the port:"
|
||||||
|
echo " sbx ports \$(hostname) --publish 3001:3001"
|
||||||
|
echo ""
|
||||||
|
echo " Then open: http://localhost:3001"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@cloudcli-ai/cloudcli",
|
||||||
"version": "1.26.2",
|
"version": "1.28.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@cloudcli-ai/cloudcli",
|
||||||
"version": "1.26.2",
|
"version": "1.28.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.59",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.59",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -69,7 +69,6 @@
|
|||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"claude-code-ui": "server/cli.js",
|
|
||||||
"cloudcli": "server/cli.js"
|
"cloudcli": "server/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@cloudcli-ai/cloudcli",
|
||||||
"version": "1.26.2",
|
"version": "1.28.0",
|
||||||
"description": "A web-based UI for Claude Code CLI",
|
"description": "A web-based UI for Claude Code CLI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"claude-code-ui": "server/cli.js",
|
|
||||||
"cloudcli": "server/cli.js"
|
"cloudcli": "server/cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -40,13 +39,24 @@
|
|||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude code",
|
"claude code",
|
||||||
"ai",
|
"claude-code",
|
||||||
|
"claude-code-ui",
|
||||||
|
"cloudcli",
|
||||||
|
"codex",
|
||||||
|
"gemini",
|
||||||
|
"gemini-cli",
|
||||||
|
"cursor",
|
||||||
|
"cursor-cli",
|
||||||
"anthropic",
|
"anthropic",
|
||||||
|
"openai",
|
||||||
|
"google",
|
||||||
|
"coding-agent",
|
||||||
|
"web-ui",
|
||||||
"ui",
|
"ui",
|
||||||
"mobile"
|
"mobile IDE"
|
||||||
],
|
],
|
||||||
"author": "CloudCLI UI Contributors",
|
"author": "CloudCLI UI Contributors",
|
||||||
"license": "GPL-3.0",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.59",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.59",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Claude Code UI - API Documentation</title>
|
<title>CloudCLI - API Documentation</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<h1>Claude Code UI</h1>
|
<h1>CloudCLI</h1>
|
||||||
<div class="subtitle">API Documentation</div>
|
<div class="subtitle">API Documentation</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,7 +585,7 @@
|
|||||||
<p>Server-sent events (SSE) format with real-time updates. Content-Type: <code>text/event-stream</code></p>
|
<p>Server-sent events (SSE) format with real-time updates. Content-Type: <code>text/event-stream</code></p>
|
||||||
|
|
||||||
<h4>Response (Non-Streaming)</h4>
|
<h4>Response (Non-Streaming)</h4>
|
||||||
<p>JSON object containing session details, assistant messages only (filtered), and token usage summary. Content-Type: <code>application/json</code></p>
|
<p>JSON object containing session details and assistant messages only (filtered). Content-Type: <code>application/json</code></p>
|
||||||
|
|
||||||
<h4>Error Response</h4>
|
<h4>Error Response</h4>
|
||||||
<p>Returns error details with appropriate HTTP status code.</p>
|
<p>Returns error details with appropriate HTTP status code.</p>
|
||||||
@@ -674,21 +674,10 @@ data: {"type":"done"}</code></pre>
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "I've completed the task..."
|
"text": "I've completed the task..."
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"usage": {
|
|
||||||
"input_tokens": 150,
|
|
||||||
"output_tokens": 50
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tokens": {
|
|
||||||
"inputTokens": 150,
|
|
||||||
"outputTokens": 50,
|
|
||||||
"cacheReadTokens": 0,
|
|
||||||
"cacheCreationTokens": 0,
|
|
||||||
"totalTokens": 200
|
|
||||||
},
|
|
||||||
"projectPath": "/path/to/project",
|
"projectPath": "/path/to/project",
|
||||||
"branch": {
|
"branch": {
|
||||||
"name": "fix-authentication-bug-abc123",
|
"name": "fix-authentication-bug-abc123",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Service Worker for Claude Code UI PWA
|
// Service Worker for CloudCLI PWA
|
||||||
// Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
|
// Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
|
||||||
// so a rebuild + refresh always picks up the latest assets.
|
// so a rebuild + refresh always picks up the latest assets.
|
||||||
const CACHE_NAME = 'claude-ui-v2';
|
const CACHE_NAME = 'claude-ui-v2';
|
||||||
@@ -79,7 +79,7 @@ self.addEventListener('push', event => {
|
|||||||
try {
|
try {
|
||||||
payload = event.data.json();
|
payload = event.data.json();
|
||||||
} catch {
|
} catch {
|
||||||
payload = { title: 'Claude Code UI', body: event.data.text() };
|
payload = { title: 'CloudCLI', body: event.data.text() };
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -92,7 +92,7 @@ self.addEventListener('push', event => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.registration.showNotification(payload.title || 'Claude Code UI', options)
|
self.registration.showNotification(payload.title || 'CloudCLI', options)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
248
redirect-package/README.md
Normal file
248
redirect-package/README.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
> ## This package has moved to [`@cloudcli-ai/cloudcli`](https://www.npmjs.com/package/@cloudcli-ai/cloudcli)
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> npm install -g @cloudcli-ai/cloudcli
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> This package (`@siteboon/claude-code-ui`) is now a thin wrapper that installs the new package automatically.
|
||||||
|
> For new installations, use `@cloudcli-ai/cloudcli` directly.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/logo.svg" alt="CloudCLI UI" width="64" height="64">
|
||||||
|
<h1>Cloud CLI (aka Claude Code UI)</h1>
|
||||||
|
<p>A desktop and mobile UI for <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>, <a href="https://docs.cursor.com/en/cli/overview">Cursor CLI</a>, <a href="https://developers.openai.com/codex">Codex</a>, and <a href="https://geminicli.com/">Gemini-CLI</a>.<br>Use it locally or remotely to view your active projects and sessions from everywhere.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://cloudcli.ai">CloudCLI Cloud</a> · <a href="https://cloudcli.ai/docs">Documentation</a> · <a href="https://discord.gg/buxwujPNRE">Discord</a> · <a href="https://github.com/siteboon/claudecodeui/issues">Bug Reports</a> · <a href="https://github.com/siteboon/claudecodeui/blob/main/CONTRIBUTING.md">Contributing</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://cloudcli.ai"><img src="https://img.shields.io/badge/☁️_CloudCLI_Cloud-Try_Now-0066FF?style=for-the-badge" alt="CloudCLI Cloud"></a>
|
||||||
|
<a href="https://discord.gg/buxwujPNRE"><img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord"></a>
|
||||||
|
<br><br>
|
||||||
|
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<h3>Desktop View</h3>
|
||||||
|
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/screenshots/desktop-main.png" alt="Desktop Interface" width="400">
|
||||||
|
<br>
|
||||||
|
<em>Main interface showing project overview and chat</em>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<h3>Mobile Experience</h3>
|
||||||
|
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/screenshots/mobile-chat.png" alt="Mobile Interface" width="250">
|
||||||
|
<br>
|
||||||
|
<em>Responsive mobile design with touch navigation</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" colspan="2">
|
||||||
|
<h3>CLI Selection</h3>
|
||||||
|
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/screenshots/cli-selection.png" alt="CLI Selection" width="400">
|
||||||
|
<br>
|
||||||
|
<em>Select between Claude Code, Gemini, Cursor CLI and Codex</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Agents from mobile
|
||||||
|
- **Interactive Chat Interface** - Built-in chat interface for seamless communication with the Agents
|
||||||
|
- **Integrated Shell Terminal** - Direct access to the Agents CLI through built-in shell functionality
|
||||||
|
- **File Explorer** - Interactive file tree with syntax highlighting and live editing
|
||||||
|
- **Git Explorer** - View, stage and commit your changes. You can also switch branches
|
||||||
|
- **Session Management** - Resume conversations, manage multiple sessions, and track history
|
||||||
|
- **Plugin System** - Extend CloudCLI with custom plugins — add new tabs, backend services, and integrations. [Build your own →](https://github.com/cloudcli-ai/cloudcli-plugin-starter)
|
||||||
|
- **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation
|
||||||
|
- **Model Compatibility** - Works with Claude, GPT, and Gemini model families (see [`shared/modelConstants.js`](https://github.com/siteboon/claudecodeui/blob/main/shared/modelConstants.js) for the full list of supported models)
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### CloudCLI Cloud (Recommended)
|
||||||
|
|
||||||
|
The fastest way to get started — no local setup required. Get a fully managed, containerized development environment accessible from the web, mobile app, API, or your favorite IDE.
|
||||||
|
|
||||||
|
**[Get started with CloudCLI Cloud](https://cloudcli.ai)**
|
||||||
|
|
||||||
|
|
||||||
|
### Self-Hosted (Open source)
|
||||||
|
|
||||||
|
Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+):
|
||||||
|
|
||||||
|
```
|
||||||
|
npx @cloudcli-ai/cloudcli
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install **globally** for regular use:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install -g @cloudcli-ai/cloudcli
|
||||||
|
cloudcli
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3001` — all your existing sessions are discovered automatically.
|
||||||
|
|
||||||
|
Visit the **[documentation →](https://cloudcli.ai/docs)** for more full configuration options, PM2, remote server setup and more
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Which option is right for you?
|
||||||
|
|
||||||
|
CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self-host it on your own machine, or use CloudCLI Cloud which builds on top of it with a full managed cloud environment, team features, and deeper integrations.
|
||||||
|
|
||||||
|
| | CloudCLI UI (Self-hosted) | CloudCLI Cloud |
|
||||||
|
|---|---|---|
|
||||||
|
| **Best for** | Developers who want a full UI for local agent sessions on their own machine | Teams and developers who want agents running in the cloud, accessible from anywhere |
|
||||||
|
| **How you access it** | Browser via `[yourip]:port` | Browser, any IDE, REST API, n8n |
|
||||||
|
| **Setup** | `npx @cloudcli-ai/cloudcli` | No setup required |
|
||||||
|
| **Machine needs to stay on** | Yes | No |
|
||||||
|
| **Mobile access** | Any browser on your network | Any device, native app coming |
|
||||||
|
| **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment |
|
||||||
|
| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI |
|
||||||
|
| **File explorer and Git** | Yes, built into the UI | Yes, built into the UI |
|
||||||
|
| **MCP configuration** | Managed via UI, synced with your local `~/.claude` config | Managed via UI |
|
||||||
|
| **IDE access** | Your local IDE | Any IDE connected to your cloud environment |
|
||||||
|
| **REST API** | Yes | Yes |
|
||||||
|
| **n8n node** | No | Yes |
|
||||||
|
| **Team sharing** | No | Yes |
|
||||||
|
| **Platform cost** | Free, open source | Starts at $7/month |
|
||||||
|
|
||||||
|
> Both options use your own AI subscriptions (Claude, Cursor, etc.) — CloudCLI provides the environment, not the AI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security & Tools Configuration
|
||||||
|
|
||||||
|
**Important Notice**: All Claude Code tools are **disabled by default**. This prevents potentially harmful operations from running automatically.
|
||||||
|
|
||||||
|
### Enabling Tools
|
||||||
|
|
||||||
|
To use Claude Code's full functionality, you'll need to manually enable tools:
|
||||||
|
|
||||||
|
1. **Open Tools Settings** - Click the gear icon in the sidebar
|
||||||
|
2. **Enable Selectively** - Turn on only the tools you need
|
||||||
|
3. **Apply Settings** - Your preferences are saved locally
|
||||||
|
|
||||||
|
**Recommended approach**: Start with basic tools enabled and add more as needed. You can always adjust these settings later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
CloudCLI has a plugin system that lets you add custom tabs with their own frontend UI and optional Node.js backend. Install plugins from git repos directly in **Settings > Plugins**, or build your own.
|
||||||
|
|
||||||
|
### Available Plugins
|
||||||
|
|
||||||
|
| Plugin | Description |
|
||||||
|
|---|---|
|
||||||
|
| **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | Shows file counts, lines of code, file-type breakdown, largest files, and recently modified files for your current project |
|
||||||
|
| **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | Full xterm.js terminal with multi-tab support|
|
||||||
|
|
||||||
|
### Build Your Own
|
||||||
|
|
||||||
|
**[Plugin Starter Template →](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** — fork this repo to create your own plugin. It includes a working example with frontend rendering, live context updates, and RPC communication to a backend server.
|
||||||
|
|
||||||
|
**[Plugin Documentation →](https://cloudcli.ai/docs/plugin-overview)** — full guide to the plugin API, manifest format, security model, and more.
|
||||||
|
|
||||||
|
---
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>How is this different from Claude Code Remote Control?</summary>
|
||||||
|
|
||||||
|
Claude Code Remote Control lets you send messages to a session already running in your local terminal. Your machine has to stay on, your terminal has to stay open, and sessions time out after roughly 10 minutes without a network connection.
|
||||||
|
|
||||||
|
CloudCLI UI and CloudCLI Cloud extend Claude Code rather than sit alongside it — your MCP servers, permissions, settings, and sessions are the exact same ones Claude Code uses natively. Nothing is duplicated or managed separately.
|
||||||
|
|
||||||
|
Here's what that means in practice:
|
||||||
|
|
||||||
|
- **All your sessions, not just one** — CloudCLI UI auto-discovers every session from your `~/.claude` folder. Remote Control only exposes the single active session to make it available in the Claude mobile app.
|
||||||
|
- **Your settings are your settings** — MCP servers, tool permissions, and project config you change in CloudCLI UI are written directly to your Claude Code config and take effect immediately, and vice versa.
|
||||||
|
- **Works with more agents** — Claude Code, Cursor CLI, Codex, and Gemini CLI, not just Claude Code.
|
||||||
|
- **Full UI, not just a chat window** — file explorer, Git integration, MCP management, and a shell terminal are all built in.
|
||||||
|
- **CloudCLI Cloud runs in the cloud** — close your laptop, the agent keeps running. No terminal to babysit, no machine to keep awake.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Do I need to pay for an AI subscription separately?</summary>
|
||||||
|
|
||||||
|
Yes. CloudCLI provides the environment, not the AI. You bring your own Claude, Cursor, Codex, or Gemini subscription. CloudCLI Cloud starts at $7/month for the hosted environment on top of that.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Can I use CloudCLI UI on my phone?</summary>
|
||||||
|
|
||||||
|
Yes. For self-hosted, run the server on your machine and open `[yourip]:port` in any browser on your network. For CloudCLI Cloud, open it from any device — no VPN, no port forwarding, no setup. A native app is also in the works.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Will changes I make in the UI affect my local Claude Code setup?</summary>
|
||||||
|
|
||||||
|
Yes, for self-hosted. CloudCLI UI reads from and writes to the same `~/.claude` config that Claude Code uses natively. MCP servers you add via the UI show up in Claude Code immediately and vice versa.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Community & Support
|
||||||
|
|
||||||
|
- **[Documentation](https://cloudcli.ai/docs)** — installation, configuration, features, and troubleshooting
|
||||||
|
- **[Discord](https://discord.gg/buxwujPNRE)** — get help and connect with other users
|
||||||
|
- **[GitHub Issues](https://github.com/siteboon/claudecodeui/issues)** — bug reports and feature requests
|
||||||
|
- **[Contributing Guide](https://github.com/siteboon/claudecodeui/blob/main/CONTRIBUTING.md)** — how to contribute to the project
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see [LICENSE](https://github.com/siteboon/claudecodeui/blob/main/LICENSE) for the full text, including additional terms under Section 7.
|
||||||
|
|
||||||
|
This project is open source and free to use, modify, and distribute under the AGPL-3.0-or-later license. If you modify this software and run it as a network service, you must make your modified source code available to users of that service.
|
||||||
|
|
||||||
|
CloudCLI UI - (https://cloudcli.ai).
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
### Built With
|
||||||
|
- **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic's official CLI
|
||||||
|
- **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor's official CLI
|
||||||
|
- **[Codex](https://developers.openai.com/codex)** - OpenAI Codex
|
||||||
|
- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI
|
||||||
|
- **[React](https://react.dev/)** - User interface library
|
||||||
|
- **[Vite](https://vitejs.dev/)** - Fast build tool and dev server
|
||||||
|
- **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework
|
||||||
|
- **[CodeMirror](https://codemirror.net/)** - Advanced code editor
|
||||||
|
- **[TaskMaster AI](https://github.com/eyaltoledano/claude-task-master)** *(Optional)* - AI-powered project management and task planning
|
||||||
|
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
- [Siteboon - AI powered website builder](https://siteboon.ai)
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<strong>Made with care for the Claude Code, Cursor and Codex community.</strong>
|
||||||
|
</div>
|
||||||
2
redirect-package/bin.js
Normal file
2
redirect-package/bin.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import('@cloudcli-ai/cloudcli/server/cli.js');
|
||||||
2
redirect-package/index.js
Normal file
2
redirect-package/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from '@cloudcli-ai/cloudcli';
|
||||||
|
export { default } from '@cloudcli-ai/cloudcli';
|
||||||
43
redirect-package/package.json
Normal file
43
redirect-package/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@siteboon/claude-code-ui",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "This package has moved to @cloudcli-ai/cloudcli",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"bin": {
|
||||||
|
"claude-code-ui": "./bin.js",
|
||||||
|
"cloudcli": "./bin.js"
|
||||||
|
},
|
||||||
|
"homepage": "https://cloudcli.ai",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/siteboon/claudecodeui.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/siteboon/claudecodeui/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"claude code",
|
||||||
|
"claude-code",
|
||||||
|
"claude-code-ui",
|
||||||
|
"cloudcli",
|
||||||
|
"codex",
|
||||||
|
"gemini",
|
||||||
|
"gemini-cli",
|
||||||
|
"cursor",
|
||||||
|
"cursor-cli",
|
||||||
|
"anthropic",
|
||||||
|
"openai",
|
||||||
|
"google",
|
||||||
|
"coding-agent",
|
||||||
|
"web-ui",
|
||||||
|
"ui",
|
||||||
|
"mobile IDE"
|
||||||
|
],
|
||||||
|
"author": "CloudCLI UI Contributors",
|
||||||
|
"dependencies": {
|
||||||
|
"@cloudcli-ai/cloudcli": "*"
|
||||||
|
},
|
||||||
|
"deprecated": "This package has been renamed to @cloudcli-ai/cloudcli. Please install @cloudcli-ai/cloudcli instead.",
|
||||||
|
"license": "AGPL-3.0-or-later"
|
||||||
|
}
|
||||||
@@ -274,46 +274,6 @@ function transformMessage(sdkMessage) {
|
|||||||
return sdkMessage;
|
return sdkMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts token usage from SDK result messages
|
|
||||||
* @param {Object} resultMessage - SDK result message
|
|
||||||
* @returns {Object|null} Token budget object or null
|
|
||||||
*/
|
|
||||||
function extractTokenBudget(resultMessage) {
|
|
||||||
if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first model's usage data
|
|
||||||
const modelKey = Object.keys(resultMessage.modelUsage)[0];
|
|
||||||
const modelData = resultMessage.modelUsage[modelKey];
|
|
||||||
|
|
||||||
if (!modelData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use cumulative tokens if available (tracks total for the session)
|
|
||||||
// Otherwise fall back to per-request tokens
|
|
||||||
const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
|
|
||||||
const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
|
|
||||||
const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
|
|
||||||
const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
|
|
||||||
|
|
||||||
// Total used = input + output + cache tokens
|
|
||||||
const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
|
|
||||||
|
|
||||||
// Use configured context window budget from environment (default 160000)
|
|
||||||
// This is the user's budget limit, not the model's context window
|
|
||||||
const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
|
|
||||||
|
|
||||||
// Token calc logged via token-budget WS event
|
|
||||||
|
|
||||||
return {
|
|
||||||
used: totalUsed,
|
|
||||||
total: contextWindow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles image processing for SDK queries
|
* Handles image processing for SDK queries
|
||||||
* Saves base64 images to temporary files and returns modified prompt with file paths
|
* Saves base64 images to temporary files and returns modified prompt with file paths
|
||||||
@@ -657,18 +617,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
}
|
}
|
||||||
ws.send(msg);
|
ws.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and send token budget updates from result messages
|
|
||||||
if (message.type === 'result') {
|
|
||||||
const models = Object.keys(message.modelUsage || {});
|
|
||||||
if (models.length > 0) {
|
|
||||||
// Model info available in result message
|
|
||||||
}
|
|
||||||
const tokenBudgetData = extractTokenBudget(message);
|
|
||||||
if (tokenBudgetData) {
|
|
||||||
ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up session on completion
|
// Clean up session on completion
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Claude Code UI CLI
|
* CloudCLI CLI
|
||||||
*
|
*
|
||||||
* Provides command-line utilities for managing Claude Code UI
|
* Provides command-line utilities for managing CloudCLI
|
||||||
*
|
*
|
||||||
* Commands:
|
* Commands:
|
||||||
* (no args) - Start the server (default)
|
* (no args) - Start the server (default)
|
||||||
@@ -84,7 +84,7 @@ function getInstallDir() {
|
|||||||
|
|
||||||
// Show status command
|
// Show status command
|
||||||
function showStatus() {
|
function showStatus() {
|
||||||
console.log(`\n${c.bright('Claude Code UI - Status')}\n`);
|
console.log(`\n${c.bright('CloudCLI UI - Status')}\n`);
|
||||||
console.log(c.dim('═'.repeat(60)));
|
console.log(c.dim('═'.repeat(60)));
|
||||||
|
|
||||||
// Version info
|
// Version info
|
||||||
@@ -141,7 +141,7 @@ function showStatus() {
|
|||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
╔═══════════════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════════════╗
|
||||||
║ Claude Code UI - Command Line Tool ║
|
║ CloudCLI - Command Line Tool ║
|
||||||
╚═══════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -149,7 +149,7 @@ Usage:
|
|||||||
cloudcli [command] [options]
|
cloudcli [command] [options]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
start Start the Claude Code UI server (default)
|
start Start the CloudCLI server (default)
|
||||||
status Show configuration and data locations
|
status Show configuration and data locations
|
||||||
update Update to the latest version
|
update Update to the latest version
|
||||||
help Show this help information
|
help Show this help information
|
||||||
@@ -203,7 +203,7 @@ function isNewerVersion(v1, v2) {
|
|||||||
async function checkForUpdates(silent = false) {
|
async function checkForUpdates(silent = false) {
|
||||||
try {
|
try {
|
||||||
const { execSync } = await import('child_process');
|
const { execSync } = await import('child_process');
|
||||||
const latestVersion = execSync('npm show @siteboon/claude-code-ui version', { encoding: 'utf8' }).trim();
|
const latestVersion = execSync('npm show @cloudcli-ai/cloudcli version', { encoding: 'utf8' }).trim();
|
||||||
const currentVersion = packageJson.version;
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
if (isNewerVersion(latestVersion, currentVersion)) {
|
if (isNewerVersion(latestVersion, currentVersion)) {
|
||||||
@@ -236,11 +236,11 @@ async function updatePackage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
|
console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
|
||||||
execSync('npm update -g @siteboon/claude-code-ui', { stdio: 'inherit' });
|
execSync('npm update -g @cloudcli-ai/cloudcli', { stdio: 'inherit' });
|
||||||
console.log(`${c.ok('[OK]')} Update complete! Restart cloudcli to use the new version.`);
|
console.log(`${c.ok('[OK]')} Update complete! Restart cloudcli to use the new version.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
|
console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
|
||||||
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @siteboon/claude-code-ui`);
|
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @cloudcli-ai/cloudcli`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
200
server/index.js
200
server/index.js
@@ -438,7 +438,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|||||||
// Run the update command based on install mode
|
// Run the update command based on install mode
|
||||||
const updateCommand = installMode === 'git'
|
const updateCommand = installMode === 'git'
|
||||||
? 'git checkout main && git pull && npm install'
|
? 'git checkout main && git pull && npm install'
|
||||||
: 'npm install -g @siteboon/claude-code-ui@latest';
|
: 'npm install -g @cloudcli-ai/cloudcli@latest';
|
||||||
|
|
||||||
const child = spawn('sh', ['-c', updateCommand], {
|
const child = spawn('sh', ['-c', updateCommand], {
|
||||||
cwd: installMode === 'git' ? projectRoot : os.homedir(),
|
cwd: installMode === 'git' ? projectRoot : os.homedir(),
|
||||||
@@ -812,7 +812,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serve binary file content endpoint (for images, etc.)
|
// Serve raw file bytes for previews and downloads.
|
||||||
app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
|
app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { projectName } = req.params;
|
const { projectName } = req.params;
|
||||||
@@ -829,7 +829,11 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|||||||
return res.status(404).json({ error: 'Project not found' });
|
return res.status(404).json({ error: 'Project not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolved = path.resolve(filePath);
|
// Match the text reader endpoint so callers can pass either project-relative
|
||||||
|
// or absolute paths without changing how the bytes are served.
|
||||||
|
const resolved = path.isAbsolute(filePath)
|
||||||
|
? path.resolve(filePath)
|
||||||
|
: path.resolve(projectRoot, filePath);
|
||||||
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
||||||
if (!resolved.startsWith(normalizedRoot)) {
|
if (!resolved.startsWith(normalizedRoot)) {
|
||||||
return res.status(403).json({ error: 'Path must be under project root' });
|
return res.status(403).json({ error: 'Path must be under project root' });
|
||||||
@@ -2214,194 +2218,6 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get token usage for a specific session
|
|
||||||
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { projectName, sessionId } = req.params;
|
|
||||||
const { provider = 'claude' } = req.query;
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
|
|
||||||
// Allow only safe characters in sessionId
|
|
||||||
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
||||||
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
|
||||||
if (provider === 'cursor') {
|
|
||||||
return res.json({
|
|
||||||
used: 0,
|
|
||||||
total: 0,
|
|
||||||
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
|
||||||
unsupported: true,
|
|
||||||
message: 'Token usage tracking not available for Cursor sessions'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Gemini sessions - they are raw logs in our current setup
|
|
||||||
if (provider === 'gemini') {
|
|
||||||
return res.json({
|
|
||||||
used: 0,
|
|
||||||
total: 0,
|
|
||||||
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
|
||||||
unsupported: true,
|
|
||||||
message: 'Token usage tracking not available for Gemini sessions'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Codex sessions
|
|
||||||
if (provider === 'codex') {
|
|
||||||
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
||||||
|
|
||||||
// Find the session file by searching for the session ID
|
|
||||||
const findSessionFile = async (dir) => {
|
|
||||||
try {
|
|
||||||
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const found = await findSessionFile(fullPath);
|
|
||||||
if (found) return found;
|
|
||||||
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Skip directories we can't read
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
|
||||||
|
|
||||||
if (!sessionFilePath) {
|
|
||||||
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and parse the Codex JSONL file
|
|
||||||
let fileContent;
|
|
||||||
try {
|
|
||||||
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const lines = fileContent.trim().split('\n');
|
|
||||||
let totalTokens = 0;
|
|
||||||
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
||||||
|
|
||||||
// Find the latest token_count event with info (scan from end)
|
|
||||||
for (let i = lines.length - 1; i >= 0; i--) {
|
|
||||||
try {
|
|
||||||
const entry = JSON.parse(lines[i]);
|
|
||||||
|
|
||||||
// Codex stores token info in event_msg with type: "token_count"
|
|
||||||
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
||||||
const tokenInfo = entry.payload.info;
|
|
||||||
if (tokenInfo.total_token_usage) {
|
|
||||||
totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
|
|
||||||
}
|
|
||||||
if (tokenInfo.model_context_window) {
|
|
||||||
contextWindow = tokenInfo.model_context_window;
|
|
||||||
}
|
|
||||||
break; // Stop after finding the latest token count
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
// Skip lines that can't be parsed
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
used: totalTokens,
|
|
||||||
total: contextWindow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Claude sessions (default)
|
|
||||||
// Extract actual project path
|
|
||||||
let projectPath;
|
|
||||||
try {
|
|
||||||
projectPath = await extractProjectDirectory(projectName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error extracting project directory:', error);
|
|
||||||
return res.status(500).json({ error: 'Failed to determine project path' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the JSONL file path
|
|
||||||
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
||||||
// The encoding replaces any non-alphanumeric character (except -) with -
|
|
||||||
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
||||||
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
||||||
|
|
||||||
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
||||||
|
|
||||||
// Constrain to projectDir
|
|
||||||
const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
|
|
||||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid path' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and parse the JSONL file
|
|
||||||
let fileContent;
|
|
||||||
try {
|
|
||||||
fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
|
|
||||||
}
|
|
||||||
throw error; // Re-throw other errors to be caught by outer try-catch
|
|
||||||
}
|
|
||||||
const lines = fileContent.trim().split('\n');
|
|
||||||
|
|
||||||
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
||||||
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
|
||||||
let inputTokens = 0;
|
|
||||||
let cacheCreationTokens = 0;
|
|
||||||
let cacheReadTokens = 0;
|
|
||||||
|
|
||||||
// Find the latest assistant message with usage data (scan from end)
|
|
||||||
for (let i = lines.length - 1; i >= 0; i--) {
|
|
||||||
try {
|
|
||||||
const entry = JSON.parse(lines[i]);
|
|
||||||
|
|
||||||
// Only count assistant messages which have usage data
|
|
||||||
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
||||||
const usage = entry.message.usage;
|
|
||||||
|
|
||||||
// Use token counts from latest assistant message only
|
|
||||||
inputTokens = usage.input_tokens || 0;
|
|
||||||
cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
||||||
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
||||||
|
|
||||||
break; // Stop after finding the latest assistant message
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
// Skip lines that can't be parsed
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total context usage (excluding output_tokens, as per ccusage)
|
|
||||||
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
used: totalUsed,
|
|
||||||
total: contextWindow,
|
|
||||||
breakdown: {
|
|
||||||
input: inputTokens,
|
|
||||||
cacheCreation: cacheCreationTokens,
|
|
||||||
cacheRead: cacheReadTokens
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading session token usage:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to read session token usage' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serve React app for all other routes (excluding static files)
|
// Serve React app for all other routes (excluding static files)
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
// Skip requests for static assets (files with extensions)
|
// Skip requests for static assets (files with extensions)
|
||||||
@@ -2544,7 +2360,7 @@ async function startServer() {
|
|||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(c.dim('═'.repeat(63)));
|
console.log(c.dim('═'.repeat(63)));
|
||||||
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
|
console.log(` ${c.bright('CloudCLI Server - Ready')}`);
|
||||||
console.log(c.dim('═'.repeat(63)));
|
console.log(c.dim('═'.repeat(63)));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
|
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ function transformCodexEvent(event) {
|
|||||||
|
|
||||||
case 'turn.completed':
|
case 'turn.completed':
|
||||||
return {
|
return {
|
||||||
type: 'turn_complete',
|
type: 'turn_complete'
|
||||||
usage: event.usage
|
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'turn.failed':
|
case 'turn.failed':
|
||||||
@@ -279,12 +278,6 @@ export async function queryCodex(command, options = {}, ws) {
|
|||||||
error: terminalFailure
|
error: terminalFailure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and send token usage if available (normalized to match Claude format)
|
|
||||||
if (event.type === 'turn.completed' && event.usage) {
|
|
||||||
const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
|
|
||||||
sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send completion event
|
// Send completion event
|
||||||
|
|||||||
@@ -1618,7 +1618,6 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
let tokenUsage = null;
|
|
||||||
const fileStream = fsSync.createReadStream(sessionFilePath);
|
const fileStream = fsSync.createReadStream(sessionFilePath);
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: fileStream,
|
input: fileStream,
|
||||||
@@ -1647,17 +1646,6 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|||||||
try {
|
try {
|
||||||
const entry = JSON.parse(line);
|
const entry = JSON.parse(line);
|
||||||
|
|
||||||
// Extract token usage from token_count events (keep latest)
|
|
||||||
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
||||||
const info = entry.payload.info;
|
|
||||||
if (info.total_token_usage) {
|
|
||||||
tokenUsage = {
|
|
||||||
used: info.total_token_usage.total_tokens || 0,
|
|
||||||
total: info.model_context_window || 200000
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use event_msg.user_message for user-visible inputs.
|
// Use event_msg.user_message for user-visible inputs.
|
||||||
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
||||||
messages.push({
|
messages.push({
|
||||||
@@ -1820,11 +1808,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|||||||
hasMore,
|
hasMore,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
tokenUsage
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { messages, tokenUsage };
|
return { messages };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ export const codexAdapter = {
|
|||||||
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
||||||
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
||||||
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
||||||
const tokenUsage = result.tokenUsage || null;
|
|
||||||
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
for (const raw of rawMessages) {
|
for (const raw of rawMessages) {
|
||||||
@@ -242,7 +241,6 @@ export const codexAdapter = {
|
|||||||
hasMore,
|
hasMore,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
tokenUsage,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,14 +53,7 @@ export function normalizeMessage(raw, sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (raw.type === 'result') {
|
if (raw.type === 'result') {
|
||||||
const msgs = [createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' })];
|
return [createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' })];
|
||||||
if (raw.stats?.total_tokens) {
|
|
||||||
msgs.push(createNormalizedMessage({
|
|
||||||
sessionId, timestamp: ts, provider: PROVIDER,
|
|
||||||
kind: 'status', text: 'Complete', tokens: raw.stats.total_tokens, canInterrupt: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return msgs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw.type === 'error') {
|
if (raw.type === 'error') {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
* - stream_end: (no extra fields)
|
* - stream_end: (no extra fields)
|
||||||
* - error: content
|
* - error: content
|
||||||
* - complete: (no extra fields)
|
* - complete: (no extra fields)
|
||||||
* - status: text, tokens?, canInterrupt?
|
* - status: text, canInterrupt?
|
||||||
* - permission_request: requestId, toolName, input, context?
|
* - permission_request: requestId, toolName, input, context?
|
||||||
* - permission_cancelled: requestId
|
* - permission_cancelled: requestId
|
||||||
* - session_created: newSessionId
|
* - session_created: newSessionId
|
||||||
@@ -66,7 +66,6 @@
|
|||||||
* @property {boolean} hasMore - Whether more messages exist before the current page
|
* @property {boolean} hasMore - Whether more messages exist before the current page
|
||||||
* @property {number} offset - Current offset
|
* @property {number} offset - Current offset
|
||||||
* @property {number|null} limit - Page size used
|
* @property {number|null} limit - Page size used
|
||||||
* @property {object} [tokenUsage] - Token usage data (provider-specific)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ─── Provider Adapter Interface ──────────────────────────────────────────────
|
// ─── Provider Adapter Interface ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ class SSEStreamWriter {
|
|||||||
|
|
||||||
setSessionId(sessionId) {
|
setSessionId(sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
|
this.send({ type: 'session-id', sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
getSessionId() {
|
getSessionId() {
|
||||||
@@ -545,7 +546,12 @@ class ResponseCollector {
|
|||||||
const parsed = JSON.parse(msg);
|
const parsed = JSON.parse(msg);
|
||||||
// Only include claude-response messages with assistant type
|
// Only include claude-response messages with assistant type
|
||||||
if (parsed.type === 'claude-response' && parsed.data && parsed.data.type === 'assistant') {
|
if (parsed.type === 'claude-response' && parsed.data && parsed.data.type === 'assistant') {
|
||||||
assistantMessages.push(parsed.data);
|
const assistantMessage = { ...parsed.data };
|
||||||
|
if (assistantMessage.message?.usage) {
|
||||||
|
assistantMessage.message = { ...assistantMessage.message };
|
||||||
|
delete assistantMessage.message.usage;
|
||||||
|
}
|
||||||
|
assistantMessages.push(assistantMessage);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not JSON, skip
|
// Not JSON, skip
|
||||||
@@ -555,49 +561,6 @@ class ResponseCollector {
|
|||||||
|
|
||||||
return assistantMessages;
|
return assistantMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate total tokens from all messages
|
|
||||||
*/
|
|
||||||
getTotalTokens() {
|
|
||||||
let totalInput = 0;
|
|
||||||
let totalOutput = 0;
|
|
||||||
let totalCacheRead = 0;
|
|
||||||
let totalCacheCreation = 0;
|
|
||||||
|
|
||||||
for (const msg of this.messages) {
|
|
||||||
let data = msg;
|
|
||||||
|
|
||||||
// Parse if string
|
|
||||||
if (typeof msg === 'string') {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(msg);
|
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract usage from claude-response messages
|
|
||||||
if (data && data.type === 'claude-response' && data.data) {
|
|
||||||
const msgData = data.data;
|
|
||||||
if (msgData.message && msgData.message.usage) {
|
|
||||||
const usage = msgData.message.usage;
|
|
||||||
totalInput += usage.input_tokens || 0;
|
|
||||||
totalOutput += usage.output_tokens || 0;
|
|
||||||
totalCacheRead += usage.cache_read_input_tokens || 0;
|
|
||||||
totalCacheCreation += usage.cache_creation_input_tokens || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
inputTokens: totalInput,
|
|
||||||
outputTokens: totalOutput,
|
|
||||||
cacheReadTokens: totalCacheRead,
|
|
||||||
cacheCreationTokens: totalCacheCreation,
|
|
||||||
totalTokens: totalInput + totalOutput + totalCacheRead + totalCacheCreation
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
@@ -788,13 +751,6 @@ class ResponseCollector {
|
|||||||
* success: true,
|
* success: true,
|
||||||
* sessionId: "session-123",
|
* sessionId: "session-123",
|
||||||
* messages: [...], // Assistant messages only (filtered)
|
* messages: [...], // Assistant messages only (filtered)
|
||||||
* tokens: {
|
|
||||||
* inputTokens: 150,
|
|
||||||
* outputTokens: 50,
|
|
||||||
* cacheReadTokens: 0,
|
|
||||||
* cacheCreationTokens: 0,
|
|
||||||
* totalTokens: 200
|
|
||||||
* },
|
|
||||||
* projectPath: "/path/to/project",
|
* projectPath: "/path/to/project",
|
||||||
* branch: { // Only if createBranch=true
|
* branch: { // Only if createBranch=true
|
||||||
* name: "feature/xyz",
|
* name: "feature/xyz",
|
||||||
@@ -839,7 +795,7 @@ class ResponseCollector {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
router.post('/', validateExternalApiKey, async (req, res) => {
|
router.post('/', validateExternalApiKey, async (req, res) => {
|
||||||
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName } = req.body;
|
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName, sessionId } = req.body;
|
||||||
|
|
||||||
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
|
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
|
||||||
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
|
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
|
||||||
@@ -949,7 +905,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
await queryClaudeSDK(message.trim(), {
|
await queryClaudeSDK(message.trim(), {
|
||||||
projectPath: finalProjectPath,
|
projectPath: finalProjectPath,
|
||||||
cwd: finalProjectPath,
|
cwd: finalProjectPath,
|
||||||
sessionId: null, // New session
|
sessionId: sessionId || null,
|
||||||
model: model,
|
model: model,
|
||||||
permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
|
permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
|
||||||
}, writer);
|
}, writer);
|
||||||
@@ -960,7 +916,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
await spawnCursor(message.trim(), {
|
await spawnCursor(message.trim(), {
|
||||||
projectPath: finalProjectPath,
|
projectPath: finalProjectPath,
|
||||||
cwd: finalProjectPath,
|
cwd: finalProjectPath,
|
||||||
sessionId: null, // New session
|
sessionId: sessionId || null,
|
||||||
model: model || undefined,
|
model: model || undefined,
|
||||||
skipPermissions: true // Bypass permissions for Cursor
|
skipPermissions: true // Bypass permissions for Cursor
|
||||||
}, writer);
|
}, writer);
|
||||||
@@ -970,7 +926,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
await queryCodex(message.trim(), {
|
await queryCodex(message.trim(), {
|
||||||
projectPath: finalProjectPath,
|
projectPath: finalProjectPath,
|
||||||
cwd: finalProjectPath,
|
cwd: finalProjectPath,
|
||||||
sessionId: null,
|
sessionId: sessionId || null,
|
||||||
model: model || CODEX_MODELS.DEFAULT,
|
model: model || CODEX_MODELS.DEFAULT,
|
||||||
permissionMode: 'bypassPermissions'
|
permissionMode: 'bypassPermissions'
|
||||||
}, writer);
|
}, writer);
|
||||||
@@ -980,7 +936,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
await spawnGemini(message.trim(), {
|
await spawnGemini(message.trim(), {
|
||||||
projectPath: finalProjectPath,
|
projectPath: finalProjectPath,
|
||||||
cwd: finalProjectPath,
|
cwd: finalProjectPath,
|
||||||
sessionId: null,
|
sessionId: sessionId || null,
|
||||||
model: model,
|
model: model,
|
||||||
skipPermissions: true // CLI mode bypasses permissions
|
skipPermissions: true // CLI mode bypasses permissions
|
||||||
}, writer);
|
}, writer);
|
||||||
@@ -1124,7 +1080,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
prBody += `Agent task: ${message}`;
|
prBody += `Agent task: ${message}`;
|
||||||
}
|
}
|
||||||
prBody += '\n\n---\n*This pull request was automatically created by Claude Code UI Agent.*';
|
prBody += '\n\n---\n*This pull request was automatically created by CloudCLI.ai Agent.*';
|
||||||
|
|
||||||
console.log(`📝 PR Title: ${prTitle}`);
|
console.log(`📝 PR Title: ${prTitle}`);
|
||||||
|
|
||||||
@@ -1172,15 +1128,13 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
// Streaming mode: end the SSE stream
|
// Streaming mode: end the SSE stream
|
||||||
writer.end();
|
writer.end();
|
||||||
} else {
|
} else {
|
||||||
// Non-streaming mode: send filtered messages and token summary as JSON
|
// Non-streaming mode: send filtered messages as JSON
|
||||||
const assistantMessages = writer.getAssistantMessages();
|
const assistantMessages = writer.getAssistantMessages();
|
||||||
const tokenSummary = writer.getTotalTokens();
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
success: true,
|
success: true,
|
||||||
sessionId: writer.getSessionId(),
|
sessionId: writer.getSessionId(),
|
||||||
messages: assistantMessages,
|
messages: assistantMessages,
|
||||||
tokens: tokenSummary,
|
|
||||||
projectPath: finalProjectPath
|
projectPath: finalProjectPath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -97,12 +97,6 @@ const builtInCommands = [
|
|||||||
namespace: 'builtin',
|
namespace: 'builtin',
|
||||||
metadata: { type: 'builtin' }
|
metadata: { type: 'builtin' }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: '/cost',
|
|
||||||
description: 'Display token usage and cost information',
|
|
||||||
namespace: 'builtin',
|
|
||||||
metadata: { type: 'builtin' }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: '/memory',
|
name: '/memory',
|
||||||
description: 'Open CLAUDE.md memory file for editing',
|
description: 'Open CLAUDE.md memory file for editing',
|
||||||
@@ -209,86 +203,6 @@ Custom commands can be created in:
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
'/cost': async (args, context) => {
|
|
||||||
const tokenUsage = context?.tokenUsage || {};
|
|
||||||
const provider = context?.provider || 'claude';
|
|
||||||
const model =
|
|
||||||
context?.model ||
|
|
||||||
(provider === 'cursor'
|
|
||||||
? CURSOR_MODELS.DEFAULT
|
|
||||||
: provider === 'codex'
|
|
||||||
? CODEX_MODELS.DEFAULT
|
|
||||||
: CLAUDE_MODELS.DEFAULT);
|
|
||||||
|
|
||||||
const used = Number(tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0) || 0;
|
|
||||||
const total =
|
|
||||||
Number(
|
|
||||||
tokenUsage.total ??
|
|
||||||
tokenUsage.contextWindow ??
|
|
||||||
parseInt(process.env.CONTEXT_WINDOW || '160000', 10),
|
|
||||||
) || 160000;
|
|
||||||
const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0;
|
|
||||||
|
|
||||||
const inputTokensRaw =
|
|
||||||
Number(
|
|
||||||
tokenUsage.inputTokens ??
|
|
||||||
tokenUsage.input ??
|
|
||||||
tokenUsage.cumulativeInputTokens ??
|
|
||||||
tokenUsage.promptTokens ??
|
|
||||||
0,
|
|
||||||
) || 0;
|
|
||||||
const outputTokens =
|
|
||||||
Number(
|
|
||||||
tokenUsage.outputTokens ??
|
|
||||||
tokenUsage.output ??
|
|
||||||
tokenUsage.cumulativeOutputTokens ??
|
|
||||||
tokenUsage.completionTokens ??
|
|
||||||
0,
|
|
||||||
) || 0;
|
|
||||||
const cacheTokens =
|
|
||||||
Number(
|
|
||||||
tokenUsage.cacheReadTokens ??
|
|
||||||
tokenUsage.cacheCreationTokens ??
|
|
||||||
tokenUsage.cacheTokens ??
|
|
||||||
tokenUsage.cachedTokens ??
|
|
||||||
0,
|
|
||||||
) || 0;
|
|
||||||
|
|
||||||
// If we only have total used tokens, treat them as input for display/estimation.
|
|
||||||
const inputTokens =
|
|
||||||
inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used;
|
|
||||||
|
|
||||||
// Rough default rates by provider (USD / 1M tokens).
|
|
||||||
const pricingByProvider = {
|
|
||||||
claude: { input: 3, output: 15 },
|
|
||||||
cursor: { input: 3, output: 15 },
|
|
||||||
codex: { input: 1.5, output: 6 },
|
|
||||||
};
|
|
||||||
const rates = pricingByProvider[provider] || pricingByProvider.claude;
|
|
||||||
|
|
||||||
const inputCost = (inputTokens / 1_000_000) * rates.input;
|
|
||||||
const outputCost = (outputTokens / 1_000_000) * rates.output;
|
|
||||||
const totalCost = inputCost + outputCost;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'builtin',
|
|
||||||
action: 'cost',
|
|
||||||
data: {
|
|
||||||
tokenUsage: {
|
|
||||||
used,
|
|
||||||
total,
|
|
||||||
percentage,
|
|
||||||
},
|
|
||||||
cost: {
|
|
||||||
input: inputCost.toFixed(4),
|
|
||||||
output: outputCost.toFixed(4),
|
|
||||||
total: totalCost.toFixed(4),
|
|
||||||
},
|
|
||||||
model,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
'/status': async (args, context) => {
|
'/status': async (args, context) => {
|
||||||
// Read version from package.json
|
// Read version from package.json
|
||||||
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ function buildPushBody(event) {
|
|||||||
const message = CODE_MAP[event.code] || 'You have a new notification';
|
const message = CODE_MAP[event.code] || 'You have a new notification';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: sessionName || 'Claude Code UI',
|
title: sessionName || 'CloudCLI',
|
||||||
body: `${providerLabel}: ${message}`,
|
body: `${providerLabel}: ${message}`,
|
||||||
data: {
|
data: {
|
||||||
sessionId: event.sessionId || null,
|
sessionId: event.sessionId || null,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useWebSocket } from '../../contexts/WebSocketContext';
|
|||||||
import { useDeviceSettings } from '../../hooks/useDeviceSettings';
|
import { useDeviceSettings } from '../../hooks/useDeviceSettings';
|
||||||
import { useSessionProtection } from '../../hooks/useSessionProtection';
|
import { useSessionProtection } from '../../hooks/useSessionProtection';
|
||||||
import { useProjectsState } from '../../hooks/useProjectsState';
|
import { useProjectsState } from '../../hooks/useProjectsState';
|
||||||
import MobileNav from './MobileNav';
|
|
||||||
|
|
||||||
export default function AppContent() {
|
export default function AppContent() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -33,7 +32,6 @@ export default function AppContent() {
|
|||||||
activeTab,
|
activeTab,
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
isLoadingProjects,
|
isLoadingProjects,
|
||||||
isInputFocused,
|
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setSidebarOpen,
|
setSidebarOpen,
|
||||||
@@ -159,7 +157,7 @@ export default function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex min-w-0 flex-1 flex-col ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<MainContent
|
<MainContent
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
selectedSession={selectedSession}
|
selectedSession={selectedSession}
|
||||||
@@ -184,14 +182,6 @@ export default function AppContent() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMobile && (
|
|
||||||
<MobileNav
|
|
||||||
activeTab={activeTab}
|
|
||||||
setActiveTab={setActiveTab}
|
|
||||||
isInputFocused={isInputFocused}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
import { useState, useRef, useEffect, type Dispatch, type SetStateAction } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
MessageSquare,
|
|
||||||
Folder,
|
|
||||||
Terminal,
|
|
||||||
GitBranch,
|
|
||||||
ClipboardCheck,
|
|
||||||
Ellipsis,
|
|
||||||
Puzzle,
|
|
||||||
Box,
|
|
||||||
Database,
|
|
||||||
Globe,
|
|
||||||
Wrench,
|
|
||||||
Zap,
|
|
||||||
BarChart3,
|
|
||||||
type LucideIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTasksSettings } from '../../contexts/TasksSettingsContext';
|
|
||||||
import { usePlugins } from '../../contexts/PluginsContext';
|
|
||||||
import { AppTab } from '../../types/app';
|
|
||||||
|
|
||||||
const PLUGIN_ICON_MAP: Record<string, LucideIcon> = {
|
|
||||||
Puzzle, Box, Database, Globe, Terminal, Wrench, Zap, BarChart3, Folder, MessageSquare, GitBranch,
|
|
||||||
};
|
|
||||||
|
|
||||||
type CoreTabId = Exclude<AppTab, `plugin:${string}` | 'preview'>;
|
|
||||||
type CoreNavItem = {
|
|
||||||
id: CoreTabId;
|
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MobileNavProps = {
|
|
||||||
activeTab: AppTab;
|
|
||||||
setActiveTab: Dispatch<SetStateAction<AppTab>>;
|
|
||||||
isInputFocused: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: MobileNavProps) {
|
|
||||||
const { t } = useTranslation(['common', 'settings']);
|
|
||||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
|
||||||
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
|
||||||
const { plugins } = usePlugins();
|
|
||||||
const [moreOpen, setMoreOpen] = useState(false);
|
|
||||||
const moreRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const enabledPlugins = plugins.filter((p) => p.enabled);
|
|
||||||
const hasPlugins = enabledPlugins.length > 0;
|
|
||||||
const isPluginActive = activeTab.startsWith('plugin:');
|
|
||||||
|
|
||||||
// Close the menu on outside tap
|
|
||||||
useEffect(() => {
|
|
||||||
if (!moreOpen) return;
|
|
||||||
const handleTap = (e: PointerEvent) => {
|
|
||||||
const target = e.target;
|
|
||||||
if (moreRef.current && target instanceof Node && !moreRef.current.contains(target)) {
|
|
||||||
setMoreOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('pointerdown', handleTap);
|
|
||||||
return () => document.removeEventListener('pointerdown', handleTap);
|
|
||||||
}, [moreOpen]);
|
|
||||||
|
|
||||||
// Close menu when a plugin tab is selected
|
|
||||||
const selectPlugin = (name: string) => {
|
|
||||||
const pluginTab = `plugin:${name}` as AppTab;
|
|
||||||
setActiveTab(pluginTab);
|
|
||||||
setMoreOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseCoreItems: CoreNavItem[] = [
|
|
||||||
{ id: 'chat', icon: MessageSquare, label: 'Chat' },
|
|
||||||
{ id: 'shell', icon: Terminal, label: 'Shell' },
|
|
||||||
{ id: 'files', icon: Folder, label: 'Files' },
|
|
||||||
{ id: 'git', icon: GitBranch, label: 'Git' },
|
|
||||||
];
|
|
||||||
const coreItems: CoreNavItem[] = shouldShowTasksTab
|
|
||||||
? [...baseCoreItems, { id: 'tasks', icon: ClipboardCheck, label: 'Tasks' }]
|
|
||||||
: baseCoreItems;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`fixed bottom-0 left-0 right-0 z-50 transform px-3 pb-[max(8px,env(safe-area-inset-bottom))] transition-transform duration-300 ease-in-out ${isInputFocused ? 'translate-y-full' : 'translate-y-0'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="nav-glass mobile-nav-float rounded-2xl border border-border/30">
|
|
||||||
<div className="flex items-center justify-around gap-0.5 px-1 py-1.5">
|
|
||||||
{coreItems.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
const isActive = activeTab === item.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setActiveTab(item.id)}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveTab(item.id);
|
|
||||||
}}
|
|
||||||
className={`relative flex flex-1 touch-manipulation flex-col items-center justify-center gap-0.5 rounded-xl px-3 py-2 transition-all duration-200 active:scale-95 ${isActive
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
aria-label={item.label}
|
|
||||||
aria-current={isActive ? 'page' : undefined}
|
|
||||||
>
|
|
||||||
{isActive && (
|
|
||||||
<div className="bg-primary/8 dark:bg-primary/12 absolute inset-0 rounded-xl" />
|
|
||||||
)}
|
|
||||||
<Icon
|
|
||||||
className={`relative z-10 transition-all duration-200 ${isActive ? 'h-5 w-5' : 'h-[18px] w-[18px]'}`}
|
|
||||||
strokeWidth={isActive ? 2.4 : 1.8}
|
|
||||||
/>
|
|
||||||
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isActive ? 'opacity-100' : 'opacity-60'}`}>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* "More" button — only shown when there are enabled plugins */}
|
|
||||||
{hasPlugins && (
|
|
||||||
<div ref={moreRef} className="relative flex-1">
|
|
||||||
<button
|
|
||||||
onClick={() => setMoreOpen((v) => !v)}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setMoreOpen((v) => !v);
|
|
||||||
}}
|
|
||||||
className={`relative flex w-full touch-manipulation flex-col items-center justify-center gap-0.5 rounded-xl px-3 py-2 transition-all duration-200 active:scale-95 ${isPluginActive || moreOpen
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
aria-label="More plugins"
|
|
||||||
aria-expanded={moreOpen}
|
|
||||||
>
|
|
||||||
{(isPluginActive && !moreOpen) && (
|
|
||||||
<div className="bg-primary/8 dark:bg-primary/12 absolute inset-0 rounded-xl" />
|
|
||||||
)}
|
|
||||||
<Ellipsis
|
|
||||||
className={`relative z-10 transition-all duration-200 ${isPluginActive ? 'h-5 w-5' : 'h-[18px] w-[18px]'}`}
|
|
||||||
strokeWidth={isPluginActive ? 2.4 : 1.8}
|
|
||||||
/>
|
|
||||||
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isPluginActive || moreOpen ? 'opacity-100' : 'opacity-60'}`}>
|
|
||||||
{t('settings:pluginSettings.morePlugins')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Popover menu */}
|
|
||||||
{moreOpen && (
|
|
||||||
<div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-full right-0 z-[60] mb-2 min-w-[180px] rounded-xl border border-border/40 bg-popover py-1.5 shadow-lg duration-150">
|
|
||||||
{enabledPlugins.map((p) => {
|
|
||||||
const Icon = PLUGIN_ICON_MAP[p.icon] || Puzzle;
|
|
||||||
const isActive = activeTab === `plugin:${p.name}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={p.name}
|
|
||||||
onClick={() => selectPlugin(p.name)}
|
|
||||||
className={`flex w-full items-center gap-2.5 px-3.5 py-2.5 text-sm transition-colors ${isActive
|
|
||||||
? 'bg-primary/8 text-primary'
|
|
||||||
: 'text-foreground hover:bg-muted/60'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4 flex-shrink-0" strokeWidth={isActive ? 2.2 : 1.8} />
|
|
||||||
<span className="truncate">{p.displayName}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ export default function AuthLoadingScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="mb-2 text-2xl font-bold text-foreground">Claude Code UI</h1>
|
<h1 className="mb-2 text-2xl font-bold text-foreground">CloudCLI</h1>
|
||||||
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
<div className="flex items-center justify-center space-x-2">
|
||||||
{loadingDotAnimationDelays.map((delay) => (
|
{loadingDotAnimationDelays.map((delay) => (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { MessageSquare } from 'lucide-react';
|
import { MessageSquare } from 'lucide-react';
|
||||||
|
import { IS_PLATFORM } from '../../../constants/config';
|
||||||
|
|
||||||
type AuthScreenLayoutProps = {
|
type AuthScreenLayoutProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -37,6 +38,22 @@ export default function AuthScreenLayout({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-muted-foreground">{footerText}</p>
|
<p className="text-sm text-muted-foreground">{footerText}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!IS_PLATFORM && (
|
||||||
|
<div className="flex items-center justify-center gap-1.5 pt-2">
|
||||||
|
<svg className="h-3.5 w-3.5 text-muted-foreground/50" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
||||||
|
</svg>
|
||||||
|
<a
|
||||||
|
href="https://github.com/siteboon/claudecodeui"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
||||||
|
>
|
||||||
|
CloudCLI is open source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function LoginForm() {
|
|||||||
<AuthScreenLayout
|
<AuthScreenLayout
|
||||||
title={t('login.title')}
|
title={t('login.title')}
|
||||||
description={t('login.description')}
|
description={t('login.description')}
|
||||||
footerText="Enter your credentials to access Claude Code UI"
|
footerText="Enter your credentials to access CloudCLI"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<AuthInputField
|
<AuthInputField
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function SetupForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthScreenLayout
|
<AuthScreenLayout
|
||||||
title="Welcome to Claude Code UI"
|
title="Welcome to CloudCLI"
|
||||||
description="Set up your account to get started"
|
description="Set up your account to get started"
|
||||||
footerText="This is a single-user system. Only one account can be created."
|
footerText="This is a single-user system. Only one account can be created."
|
||||||
logo={<img src="/logo.svg" alt="CloudCLI" className="h-16 w-16" />}
|
logo={<img src="/logo.svg" alt="CloudCLI" className="h-16 w-16" />}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ interface UseChatComposerStateArgs {
|
|||||||
geminiModel: string;
|
geminiModel: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
canAbortSession: boolean;
|
canAbortSession: boolean;
|
||||||
tokenBudget: Record<string, unknown> | null;
|
|
||||||
sendMessage: (message: unknown) => void;
|
sendMessage: (message: unknown) => void;
|
||||||
sendByCtrlEnter?: boolean;
|
sendByCtrlEnter?: boolean;
|
||||||
onSessionActive?: (sessionId?: string | null) => void;
|
onSessionActive?: (sessionId?: string | null) => void;
|
||||||
@@ -57,7 +56,7 @@ interface UseChatComposerStateArgs {
|
|||||||
rewindMessages: (count: number) => void;
|
rewindMessages: (count: number) => void;
|
||||||
setIsLoading: (loading: boolean) => void;
|
setIsLoading: (loading: boolean) => void;
|
||||||
setCanAbortSession: (canAbort: boolean) => void;
|
setCanAbortSession: (canAbort: boolean) => void;
|
||||||
setClaudeStatus: (status: { text: string; tokens: number; can_interrupt: boolean } | null) => void;
|
setClaudeStatus: (status: { text: string; can_interrupt: boolean } | null) => void;
|
||||||
setIsUserScrolledUp: (isScrolledUp: boolean) => void;
|
setIsUserScrolledUp: (isScrolledUp: boolean) => void;
|
||||||
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
||||||
}
|
}
|
||||||
@@ -114,7 +113,6 @@ export function useChatComposerState({
|
|||||||
geminiModel,
|
geminiModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
canAbortSession,
|
canAbortSession,
|
||||||
tokenBudget,
|
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
onSessionActive,
|
onSessionActive,
|
||||||
@@ -176,12 +174,6 @@ export function useChatComposerState({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cost': {
|
|
||||||
const costMessage = `**Token Usage**: ${data.tokenUsage.used.toLocaleString()} / ${data.tokenUsage.total.toLocaleString()} (${data.tokenUsage.percentage}%)\n\n**Estimated Cost**:\n- Input: $${data.cost.input}\n- Output: $${data.cost.output}\n- **Total**: $${data.cost.total}\n\n**Model**: ${data.model}`;
|
|
||||||
addMessage({ type: 'assistant', content: costMessage, timestamp: Date.now() });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'status': {
|
case 'status': {
|
||||||
const statusMessage = `**System Status**\n\n- Version: ${data.version}\n- Uptime: ${data.uptime}\n- Model: ${data.model}\n- Provider: ${data.provider}\n- Node.js: ${data.nodeVersion}\n- Platform: ${data.platform}`;
|
const statusMessage = `**System Status**\n\n- Version: ${data.version}\n- Uptime: ${data.uptime}\n- Model: ${data.model}\n- Provider: ${data.provider}\n- Node.js: ${data.nodeVersion}\n- Platform: ${data.platform}`;
|
||||||
addMessage({ type: 'assistant', content: statusMessage, timestamp: Date.now() });
|
addMessage({ type: 'assistant', content: statusMessage, timestamp: Date.now() });
|
||||||
@@ -282,7 +274,6 @@ export function useChatComposerState({
|
|||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
provider,
|
provider,
|
||||||
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : claudeModel,
|
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : claudeModel,
|
||||||
tokenUsage: tokenBudget,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await authenticatedFetch('/api/commands/execute', {
|
const response = await authenticatedFetch('/api/commands/execute', {
|
||||||
@@ -339,7 +330,6 @@ export function useChatComposerState({
|
|||||||
provider,
|
provider,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
addMessage,
|
addMessage,
|
||||||
tokenBudget,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -543,7 +533,6 @@ export function useChatComposerState({
|
|||||||
setCanAbortSession(true);
|
setCanAbortSession(true);
|
||||||
setClaudeStatus({
|
setClaudeStatus({
|
||||||
text: 'Processing',
|
text: 'Processing',
|
||||||
tokens: 0,
|
|
||||||
can_interrupt: true,
|
can_interrupt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ type LatestChatMessage = {
|
|||||||
provider?: string;
|
provider?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
tokens?: number;
|
|
||||||
canInterrupt?: boolean;
|
canInterrupt?: boolean;
|
||||||
tokenBudget?: unknown;
|
|
||||||
newSessionId?: string;
|
newSessionId?: string;
|
||||||
aborted?: boolean;
|
aborted?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@@ -55,8 +53,7 @@ interface UseChatRealtimeHandlersArgs {
|
|||||||
setCurrentSessionId: (sessionId: string | null) => void;
|
setCurrentSessionId: (sessionId: string | null) => void;
|
||||||
setIsLoading: (loading: boolean) => void;
|
setIsLoading: (loading: boolean) => void;
|
||||||
setCanAbortSession: (canAbort: boolean) => void;
|
setCanAbortSession: (canAbort: boolean) => void;
|
||||||
setClaudeStatus: (status: { text: string; tokens: number; can_interrupt: boolean } | null) => void;
|
setClaudeStatus: (status: { text: string; can_interrupt: boolean } | null) => void;
|
||||||
setTokenBudget: (budget: Record<string, unknown> | null) => void;
|
|
||||||
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
||||||
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
||||||
streamBufferRef: MutableRefObject<string>;
|
streamBufferRef: MutableRefObject<string>;
|
||||||
@@ -85,7 +82,6 @@ export function useChatRealtimeHandlers({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
setTokenBudget,
|
|
||||||
setPendingPermissionRequests,
|
setPendingPermissionRequests,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
streamBufferRef,
|
streamBufferRef,
|
||||||
@@ -140,7 +136,6 @@ export function useChatRealtimeHandlers({
|
|||||||
if (status) {
|
if (status) {
|
||||||
const statusInfo = {
|
const statusInfo = {
|
||||||
text: status.text || 'Working...',
|
text: status.text || 'Working...',
|
||||||
tokens: status.tokens || 0,
|
|
||||||
can_interrupt: status.can_interrupt !== undefined ? status.can_interrupt : true,
|
can_interrupt: status.can_interrupt !== undefined ? status.can_interrupt : true,
|
||||||
};
|
};
|
||||||
setClaudeStatus(statusInfo);
|
setClaudeStatus(statusInfo);
|
||||||
@@ -311,7 +306,7 @@ export function useChatRealtimeHandlers({
|
|||||||
});
|
});
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setCanAbortSession(true);
|
setCanAbortSession(true);
|
||||||
setClaudeStatus({ text: 'Waiting for permission', tokens: 0, can_interrupt: true });
|
setClaudeStatus({ text: 'Waiting for permission', can_interrupt: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,12 +318,9 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'status': {
|
case 'status': {
|
||||||
if (msg.text === 'token_budget' && msg.tokenBudget) {
|
if (msg.text) {
|
||||||
setTokenBudget(msg.tokenBudget as Record<string, unknown>);
|
|
||||||
} else if (msg.text) {
|
|
||||||
setClaudeStatus({
|
setClaudeStatus({
|
||||||
text: msg.text,
|
text: msg.text,
|
||||||
tokens: msg.tokens || 0,
|
|
||||||
can_interrupt: msg.canInterrupt !== undefined ? msg.canInterrupt : true,
|
can_interrupt: msg.canInterrupt !== undefined ? msg.canInterrupt : true,
|
||||||
});
|
});
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -352,7 +344,6 @@ export function useChatRealtimeHandlers({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
setTokenBudget,
|
|
||||||
setPendingPermissionRequests,
|
setPendingPermissionRequests,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
streamBufferRef,
|
streamBufferRef,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
import { authenticatedFetch } from '../../../utils/api';
|
|
||||||
import type { ChatMessage, Provider } from '../types/types';
|
import type { ChatMessage, Provider } from '../types/types';
|
||||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||||
import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
||||||
@@ -108,9 +107,8 @@ export function useChatSessionState({
|
|||||||
const [totalMessages, setTotalMessages] = useState(0);
|
const [totalMessages, setTotalMessages] = useState(0);
|
||||||
const [canAbortSession, setCanAbortSession] = useState(false);
|
const [canAbortSession, setCanAbortSession] = useState(false);
|
||||||
const [isUserScrolledUp, setIsUserScrolledUp] = useState(false);
|
const [isUserScrolledUp, setIsUserScrolledUp] = useState(false);
|
||||||
const [tokenBudget, setTokenBudget] = useState<Record<string, unknown> | null>(null);
|
|
||||||
const [visibleMessageCount, setVisibleMessageCount] = useState(INITIAL_VISIBLE_MESSAGES);
|
const [visibleMessageCount, setVisibleMessageCount] = useState(INITIAL_VISIBLE_MESSAGES);
|
||||||
const [claudeStatus, setClaudeStatus] = useState<{ text: string; tokens: number; can_interrupt: boolean } | null>(null);
|
const [claudeStatus, setClaudeStatus] = useState<{ text: string; can_interrupt: boolean } | null>(null);
|
||||||
const [allMessagesLoaded, setAllMessagesLoaded] = useState(false);
|
const [allMessagesLoaded, setAllMessagesLoaded] = useState(false);
|
||||||
const [isLoadingAllMessages, setIsLoadingAllMessages] = useState(false);
|
const [isLoadingAllMessages, setIsLoadingAllMessages] = useState(false);
|
||||||
const [loadAllJustFinished, setLoadAllJustFinished] = useState(false);
|
const [loadAllJustFinished, setLoadAllJustFinished] = useState(false);
|
||||||
@@ -319,7 +317,6 @@ export function useChatSessionState({
|
|||||||
messagesOffsetRef.current = 0;
|
messagesOffsetRef.current = 0;
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
setTotalMessages(0);
|
setTotalMessages(0);
|
||||||
setTokenBudget(null);
|
|
||||||
lastLoadedSessionKeyRef.current = null;
|
lastLoadedSessionKeyRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -355,7 +352,6 @@ export function useChatSessionState({
|
|||||||
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
||||||
|
|
||||||
if (sessionChanged) {
|
if (sessionChanged) {
|
||||||
setTokenBudget(null);
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +379,6 @@ export function useChatSessionState({
|
|||||||
if (slot) {
|
if (slot) {
|
||||||
setHasMoreMessages(slot.hasMore);
|
setHasMoreMessages(slot.hasMore);
|
||||||
setTotalMessages(slot.total);
|
setTotalMessages(slot.total);
|
||||||
if (slot.tokenUsage) setTokenBudget(slot.tokenUsage as Record<string, unknown>);
|
|
||||||
}
|
}
|
||||||
setIsLoadingSessionMessages(false);
|
setIsLoadingSessionMessages(false);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
@@ -539,31 +534,6 @@ export function useChatSessionState({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [chatMessages.length, isLoadingSessionMessages, searchTarget]);
|
}, [chatMessages.length, isLoadingSessionMessages, searchTarget]);
|
||||||
|
|
||||||
// Token usage fetch for Claude
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedProject || !selectedSession?.id || selectedSession.id.startsWith('new-session-')) {
|
|
||||||
setTokenBudget(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sessionProvider = selectedSession.__provider || 'claude';
|
|
||||||
if (sessionProvider !== 'claude') return;
|
|
||||||
|
|
||||||
const fetchInitialTokenUsage = async () => {
|
|
||||||
try {
|
|
||||||
const url = `/api/projects/${selectedProject.name}/sessions/${selectedSession.id}/token-usage`;
|
|
||||||
const response = await authenticatedFetch(url);
|
|
||||||
if (response.ok) {
|
|
||||||
setTokenBudget(await response.json());
|
|
||||||
} else {
|
|
||||||
setTokenBudget(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch initial token usage:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchInitialTokenUsage();
|
|
||||||
}, [selectedProject, selectedSession?.id, selectedSession?.__provider]);
|
|
||||||
|
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
||||||
return chatMessages.slice(-visibleMessageCount);
|
return chatMessages.slice(-visibleMessageCount);
|
||||||
@@ -713,8 +683,6 @@ export function useChatSessionState({
|
|||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
isUserScrolledUp,
|
isUserScrolledUp,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
tokenBudget,
|
|
||||||
setTokenBudget,
|
|
||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ export const ToolDiffViewer: React.FC<ToolDiffViewerProps> = ({
|
|||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400';
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400';
|
||||||
|
|
||||||
const diffLines = useMemo(
|
const diffLines = useMemo(
|
||||||
() => createDiff(oldContent, newContent),
|
() => {
|
||||||
|
if (oldContent === undefined || newContent === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return createDiff(oldContent, newContent)
|
||||||
|
},
|
||||||
[createDiff, oldContent, newContent]
|
[createDiff, oldContent, newContent]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,6 @@ function ChatInterface({
|
|||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
isUserScrolledUp,
|
isUserScrolledUp,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
tokenBudget,
|
|
||||||
setTokenBudget,
|
|
||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
@@ -183,7 +181,6 @@ function ChatInterface({
|
|||||||
geminiModel,
|
geminiModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
canAbortSession,
|
canAbortSession,
|
||||||
tokenBudget,
|
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
onSessionActive,
|
onSessionActive,
|
||||||
@@ -227,7 +224,6 @@ function ChatInterface({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
setTokenBudget,
|
|
||||||
setPendingPermissionRequests,
|
setPendingPermissionRequests,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
streamBufferRef,
|
streamBufferRef,
|
||||||
@@ -338,7 +334,6 @@ function ChatInterface({
|
|||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChatComposer
|
<ChatComposer
|
||||||
@@ -353,7 +348,6 @@ function ChatInterface({
|
|||||||
onModeSwitch={cyclePermissionMode}
|
onModeSwitch={cyclePermissionMode}
|
||||||
thinkingMode={thinkingMode}
|
thinkingMode={thinkingMode}
|
||||||
setThinkingMode={setThinkingMode}
|
setThinkingMode={setThinkingMode}
|
||||||
tokenBudget={tokenBudget}
|
|
||||||
slashCommandsCount={slashCommandsCount}
|
slashCommandsCount={slashCommandsCount}
|
||||||
onToggleCommandMenu={handleToggleCommandMenu}
|
onToggleCommandMenu={handleToggleCommandMenu}
|
||||||
hasInput={Boolean(input.trim())}
|
hasInput={Boolean(input.trim())}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { SessionProvider } from '../../../../types/app';
|
|
||||||
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
|
||||||
|
|
||||||
type AssistantThinkingIndicatorProps = {
|
|
||||||
selectedProvider: SessionProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function AssistantThinkingIndicator({ selectedProvider }: AssistantThinkingIndicatorProps) {
|
|
||||||
return (
|
|
||||||
<div className="chat-message assistant">
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mb-2 flex items-center space-x-3">
|
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-transparent p-1 text-sm text-white">
|
|
||||||
<SessionProviderLogo provider={selectedProvider} className="h-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : selectedProvider === 'gemini' ? 'Gemini' : 'Claude'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full pl-3 text-sm text-gray-500 dark:text-gray-400 sm:pl-0">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<div className="animate-pulse">.</div>
|
|
||||||
<div className="animate-pulse" style={{ animationDelay: '0.2s' }}>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
<div className="animate-pulse" style={{ animationDelay: '0.4s' }}>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
<span className="ml-2">Thinking...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ interface ChatComposerProps {
|
|||||||
decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
||||||
) => void;
|
) => void;
|
||||||
handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
||||||
claudeStatus: { text: string; tokens: number; can_interrupt: boolean } | null;
|
claudeStatus: { text: string; can_interrupt: boolean } | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onAbortSession: () => void;
|
onAbortSession: () => void;
|
||||||
provider: Provider | string;
|
provider: Provider | string;
|
||||||
@@ -49,7 +49,6 @@ interface ChatComposerProps {
|
|||||||
onModeSwitch: () => void;
|
onModeSwitch: () => void;
|
||||||
thinkingMode: string;
|
thinkingMode: string;
|
||||||
setThinkingMode: Dispatch<SetStateAction<string>>;
|
setThinkingMode: Dispatch<SetStateAction<string>>;
|
||||||
tokenBudget: { used?: number; total?: number } | null;
|
|
||||||
slashCommandsCount: number;
|
slashCommandsCount: number;
|
||||||
onToggleCommandMenu: () => void;
|
onToggleCommandMenu: () => void;
|
||||||
hasInput: boolean;
|
hasInput: boolean;
|
||||||
@@ -106,7 +105,6 @@ export default function ChatComposer({
|
|||||||
onModeSwitch,
|
onModeSwitch,
|
||||||
thinkingMode,
|
thinkingMode,
|
||||||
setThinkingMode,
|
setThinkingMode,
|
||||||
tokenBudget,
|
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
onToggleCommandMenu,
|
onToggleCommandMenu,
|
||||||
hasInput,
|
hasInput,
|
||||||
@@ -194,7 +192,6 @@ export default function ChatComposer({
|
|||||||
provider={provider}
|
provider={provider}
|
||||||
thinkingMode={thinkingMode}
|
thinkingMode={thinkingMode}
|
||||||
setThinkingMode={setThinkingMode}
|
setThinkingMode={setThinkingMode}
|
||||||
tokenBudget={tokenBudget}
|
|
||||||
slashCommandsCount={slashCommandsCount}
|
slashCommandsCount={slashCommandsCount}
|
||||||
onToggleCommandMenu={onToggleCommandMenu}
|
onToggleCommandMenu={onToggleCommandMenu}
|
||||||
hasInput={hasInput}
|
hasInput={hasInput}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { PermissionMode, Provider } from '../../types/types';
|
import type { PermissionMode, Provider } from '../../types/types';
|
||||||
import ThinkingModeSelector from './ThinkingModeSelector';
|
import ThinkingModeSelector from './ThinkingModeSelector';
|
||||||
import TokenUsagePie from './TokenUsagePie';
|
|
||||||
|
|
||||||
interface ChatInputControlsProps {
|
interface ChatInputControlsProps {
|
||||||
permissionMode: PermissionMode | string;
|
permissionMode: PermissionMode | string;
|
||||||
@@ -10,7 +9,6 @@ interface ChatInputControlsProps {
|
|||||||
provider: Provider | string;
|
provider: Provider | string;
|
||||||
thinkingMode: string;
|
thinkingMode: string;
|
||||||
setThinkingMode: React.Dispatch<React.SetStateAction<string>>;
|
setThinkingMode: React.Dispatch<React.SetStateAction<string>>;
|
||||||
tokenBudget: { used?: number; total?: number } | null;
|
|
||||||
slashCommandsCount: number;
|
slashCommandsCount: number;
|
||||||
onToggleCommandMenu: () => void;
|
onToggleCommandMenu: () => void;
|
||||||
hasInput: boolean;
|
hasInput: boolean;
|
||||||
@@ -26,7 +24,6 @@ export default function ChatInputControls({
|
|||||||
provider,
|
provider,
|
||||||
thinkingMode,
|
thinkingMode,
|
||||||
setThinkingMode,
|
setThinkingMode,
|
||||||
tokenBudget,
|
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
onToggleCommandMenu,
|
onToggleCommandMenu,
|
||||||
hasInput,
|
hasInput,
|
||||||
@@ -78,8 +75,6 @@ export default function ChatInputControls({
|
|||||||
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
|
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TokenUsagePie used={tokenBudget?.used || 0} total={tokenBudget?.total || parseInt(import.meta.env.VITE_CONTEXT_WINDOW) || 160000} />
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggleCommandMenu}
|
onClick={onToggleCommandMenu}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { Project, ProjectSession, SessionProvider } from '../../../../types
|
|||||||
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
||||||
import MessageComponent from './MessageComponent';
|
import MessageComponent from './MessageComponent';
|
||||||
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
||||||
import AssistantThinkingIndicator from './AssistantThinkingIndicator';
|
|
||||||
|
|
||||||
interface ChatMessagesPaneProps {
|
interface ChatMessagesPaneProps {
|
||||||
scrollContainerRef: RefObject<HTMLDivElement>;
|
scrollContainerRef: RefObject<HTMLDivElement>;
|
||||||
@@ -51,7 +50,6 @@ interface ChatMessagesPaneProps {
|
|||||||
showRawParameters?: boolean;
|
showRawParameters?: boolean;
|
||||||
showThinking?: boolean;
|
showThinking?: boolean;
|
||||||
selectedProject: Project;
|
selectedProject: Project;
|
||||||
isLoading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatMessagesPane({
|
export default function ChatMessagesPane({
|
||||||
@@ -97,7 +95,6 @@ export default function ChatMessagesPane({
|
|||||||
showRawParameters,
|
showRawParameters,
|
||||||
showThinking,
|
showThinking,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
isLoading,
|
|
||||||
}: ChatMessagesPaneProps) {
|
}: ChatMessagesPaneProps) {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
const messageKeyMapRef = useRef<WeakMap<ChatMessage, string>>(new WeakMap());
|
const messageKeyMapRef = useRef<WeakMap<ChatMessage, string>>(new WeakMap());
|
||||||
@@ -261,8 +258,6 @@ export default function ChatMessagesPane({
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && <AssistantThinkingIndicator selectedProvider={provider} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'
|
|||||||
type ClaudeStatusProps = {
|
type ClaudeStatusProps = {
|
||||||
status: {
|
status: {
|
||||||
text?: string;
|
text?: string;
|
||||||
tokens?: number;
|
|
||||||
can_interrupt?: boolean;
|
can_interrupt?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
onAbort?: () => void;
|
onAbort?: () => void;
|
||||||
@@ -23,7 +22,6 @@ const ACTION_KEYS = [
|
|||||||
'claudeStatus.actions.reasoning',
|
'claudeStatus.actions.reasoning',
|
||||||
];
|
];
|
||||||
const DEFAULT_ACTION_WORDS = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning'];
|
const DEFAULT_ACTION_WORDS = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning'];
|
||||||
const ANIMATION_STEPS = 40;
|
|
||||||
|
|
||||||
const PROVIDER_LABEL_KEYS: Record<string, string> = {
|
const PROVIDER_LABEL_KEYS: Record<string, string> = {
|
||||||
claude: 'messageTypes.claude',
|
claude: 'messageTypes.claude',
|
||||||
@@ -32,19 +30,10 @@ const PROVIDER_LABEL_KEYS: Record<string, string> = {
|
|||||||
gemini: 'messageTypes.gemini',
|
gemini: 'messageTypes.gemini',
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatElapsedTime(totalSeconds: number, t: (key: string, options?: Record<string, unknown>) => string) {
|
function formatElapsedTime(totalSeconds: number) {
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
const mins = Math.floor(totalSeconds / 60);
|
||||||
const seconds = totalSeconds % 60;
|
const secs = totalSeconds % 60;
|
||||||
|
return mins < 1 ? `${secs}s` : `${mins}m ${secs}s`;
|
||||||
if (minutes < 1) {
|
|
||||||
return t('claudeStatus.elapsed.seconds', { count: seconds, defaultValue: '{{count}}s' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return t('claudeStatus.elapsed.minutesSeconds', {
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
defaultValue: '{{minutes}}m {{seconds}}s',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ClaudeStatus({
|
export default function ClaudeStatus({
|
||||||
@@ -55,143 +44,85 @@ export default function ClaudeStatus({
|
|||||||
}: ClaudeStatusProps) {
|
}: ClaudeStatusProps) {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
const [elapsedTime, setElapsedTime] = useState(0);
|
const [elapsedTime, setElapsedTime] = useState(0);
|
||||||
const [animationPhase, setAnimationPhase] = useState(0);
|
const [dots, setDots] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
setElapsedTime(0);
|
setElapsedTime(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const timer = setInterval(() => {
|
||||||
const timer = window.setInterval(() => {
|
setElapsedTime(Math.floor((Date.now() - startTime) / 1000));
|
||||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
||||||
setElapsedTime(elapsed);
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
const dotTimer = setInterval(() => {
|
||||||
return () => window.clearInterval(timer);
|
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
|
||||||
}, [isLoading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
setAnimationPhase((previous) => (previous + 1) % ANIMATION_STEPS);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
clearInterval(dotTimer);
|
||||||
|
};
|
||||||
}, [isLoading]);
|
}, [isLoading]);
|
||||||
|
|
||||||
// Note: showThinking only controls the reasoning accordion in messages, not this processing indicator
|
if (!isLoading && !status) return null;
|
||||||
if (!isLoading && !status) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionWords = ACTION_KEYS.map((key, index) => t(key, { defaultValue: DEFAULT_ACTION_WORDS[index] }));
|
const actionWords = ACTION_KEYS.map((key, i) => t(key, { defaultValue: DEFAULT_ACTION_WORDS[i] }));
|
||||||
const actionIndex = Math.floor(elapsedTime / 3) % actionWords.length;
|
const statusText = (status?.text || actionWords[Math.floor(elapsedTime / 3) % actionWords.length]).replace(/[.]+$/, '');
|
||||||
const statusText = status?.text || actionWords[actionIndex];
|
|
||||||
const cleanStatusText = statusText.replace(/[.]+$/, '');
|
const providerLabel = t(PROVIDER_LABEL_KEYS[provider] || 'claudeStatus.providers.assistant', { defaultValue: 'Assistant' });
|
||||||
const canInterrupt = isLoading && status?.can_interrupt !== false;
|
|
||||||
const providerLabelKey = PROVIDER_LABEL_KEYS[provider];
|
|
||||||
const providerLabel = providerLabelKey
|
|
||||||
? t(providerLabelKey)
|
|
||||||
: t('claudeStatus.providers.assistant', { defaultValue: 'Assistant' });
|
|
||||||
const animatedDots = '.'.repeat((animationPhase % 3) + 1);
|
|
||||||
const elapsedLabel =
|
|
||||||
elapsedTime > 0
|
|
||||||
? t('claudeStatus.elapsed.label', {
|
|
||||||
time: formatElapsedTime(elapsedTime, t),
|
|
||||||
defaultValue: '{{time}} elapsed',
|
|
||||||
})
|
|
||||||
: t('claudeStatus.elapsed.startingNow', { defaultValue: 'Starting now' });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in slide-in-from-bottom mb-3 w-full duration-300 sm:mb-6">
|
<div className="animate-in fade-in slide-in-from-bottom-2 mb-3 w-full duration-500">
|
||||||
<div className="relative mx-auto max-w-4xl overflow-hidden rounded-2xl border border-border/70 bg-card/90 shadow-md backdrop-blur-md">
|
<div className="mx-auto flex max-w-4xl items-center justify-between gap-3 overflow-hidden rounded-full border border-border/50 bg-slate-100 px-3 py-1.5 shadow-sm backdrop-blur-md dark:bg-slate-900">
|
||||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-primary/10 via-transparent to-sky-500/10 dark:from-primary/20 dark:to-sky-400/20" />
|
|
||||||
|
|
||||||
<div className="relative px-3 py-3 sm:px-4 sm:py-3.5">
|
{/* Left Side: Identity & Status */}
|
||||||
<div className="flex flex-col gap-2.5 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex min-w-0 items-center gap-2.5">
|
||||||
<div className="flex min-w-0 items-start gap-3" role="status" aria-live="polite">
|
<div className="relative flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/20 ring-1 ring-primary/10">
|
||||||
<div className="relative mt-0.5 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl border border-primary/25 bg-primary/10">
|
<SessionProviderLogo provider={provider} className="h-3.5 w-3.5" />
|
||||||
<SessionProviderLogo provider={provider} className="h-5 w-5" />
|
|
||||||
<span className="absolute -right-0.5 -top-0.5 flex h-2.5 w-2.5">
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400/70" />
|
<span className="absolute inset-0 animate-pulse rounded-full ring-2 ring-emerald-500/20" />
|
||||||
)}
|
)}
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex h-2.5 w-2.5 rounded-full',
|
|
||||||
isLoading ? 'bg-emerald-400' : 'bg-amber-400',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="flex min-w-0 flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||||
<div className="mb-0.5 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.15em] text-muted-foreground">
|
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70">
|
||||||
<span>{providerLabel}</span>
|
{providerLabel}
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'rounded-full px-2 py-0.5 text-[9px] tracking-[0.14em]',
|
|
||||||
isLoading
|
|
||||||
? 'bg-emerald-500/15 text-emerald-500 dark:text-emerald-400'
|
|
||||||
: 'bg-amber-500/15 text-amber-600 dark:text-amber-400',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isLoading
|
|
||||||
? t('claudeStatus.state.live', { defaultValue: 'Live' })
|
|
||||||
: t('claudeStatus.state.paused', { defaultValue: 'Paused' })}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={cn("h-1.5 w-1.5 rounded-full", isLoading ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||||
<p className="truncate text-sm font-semibold text-foreground sm:text-[15px]">
|
<p className="truncate text-xs font-medium text-foreground">
|
||||||
{cleanStatusText}
|
{statusText}<span className="inline-block w-4 text-primary">{isLoading ? dots : ''}</span>
|
||||||
{isLoading && (
|
|
||||||
<span aria-hidden="true" className="text-primary">
|
|
||||||
{animatedDots}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground sm:text-xs">
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="-ml-2 inline-flex items-center rounded-full border border-border/70 bg-background/60 px-2 py-0.5"
|
|
||||||
>
|
|
||||||
{elapsedLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canInterrupt && onAbort && (
|
{/* Right Side: Metrics & Actions */}
|
||||||
<div className="w-full sm:w-auto sm:text-right">
|
<div className="flex items-center gap-2">
|
||||||
|
{isLoading && status?.can_interrupt !== false && onAbort && (
|
||||||
|
<>
|
||||||
|
<div className="hidden items-center rounded-md bg-muted/50 px-2 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground sm:flex">
|
||||||
|
{formatElapsedTime(elapsedTime)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAbort}
|
onClick={onAbort}
|
||||||
className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-destructive px-3.5 py-2 text-sm font-semibold text-destructive-foreground shadow-sm ring-1 ring-destructive/40 transition-opacity hover:opacity-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/70 active:opacity-90 sm:w-auto"
|
className="group flex items-center gap-1.5 rounded-full bg-destructive/10 px-2.5 py-1 text-[10px] font-bold text-destructive transition-all hover:bg-destructive hover:text-destructive-foreground"
|
||||||
>
|
>
|
||||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="h-3 w-3 fill-current" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<path d="M6 6h12v12H6z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{t('claudeStatus.controls.stopGeneration', { defaultValue: 'Stop Generation' })}</span>
|
<span className="hidden sm:inline">STOP</span>
|
||||||
<span className="rounded-md bg-black/20 px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-destructive-foreground/95">
|
<kbd className="hidden rounded bg-black/10 px-1 text-[9px] group-hover:bg-white/20 sm:block">
|
||||||
Esc
|
ESC
|
||||||
</span>
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
<p className="mt-1 hidden text-[11px] text-muted-foreground sm:block">
|
|
||||||
{t('claudeStatus.controls.pressEscToStop', { defaultValue: 'Press Esc anytime to stop' })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
type TokenUsagePieProps = {
|
|
||||||
used: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TokenUsagePie({ used, total }: TokenUsagePieProps) {
|
|
||||||
// Token usage visualization component
|
|
||||||
// Only bail out on missing values or non‐positive totals; allow used===0 to render 0%
|
|
||||||
if (used == null || total == null || total <= 0) return null;
|
|
||||||
|
|
||||||
const percentage = Math.min(100, (used / total) * 100);
|
|
||||||
const radius = 10;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
const offset = circumference - (percentage / 100) * circumference;
|
|
||||||
|
|
||||||
// Color based on usage level
|
|
||||||
const getColor = () => {
|
|
||||||
if (percentage < 50) return '#3b82f6'; // blue
|
|
||||||
if (percentage < 75) return '#f59e0b'; // orange
|
|
||||||
return '#ef4444'; // red
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" className="-rotate-90 transform">
|
|
||||||
{/* Background circle */}
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r={radius}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
className="text-gray-300 dark:text-gray-600"
|
|
||||||
/>
|
|
||||||
{/* Progress circle */}
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r={radius}
|
|
||||||
fill="none"
|
|
||||||
stroke={getColor()}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeDasharray={circumference}
|
|
||||||
strokeDashoffset={offset}
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span title={`${used.toLocaleString()} / ${total.toLocaleString()} tokens`}>
|
|
||||||
{percentage.toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -248,6 +248,20 @@ export function useFileTreeOperations({
|
|||||||
showToast(t('fileTree.toast.pathCopied', 'Path copied to clipboard'), 'success');
|
showToast(t('fileTree.toast.pathCopied', 'Path copied to clipboard'), 'success');
|
||||||
}, [showToast, t]);
|
}, [showToast, t]);
|
||||||
|
|
||||||
|
const triggerBrowserDownload = useCallback((blob: Blob, fileName: string) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = fileName;
|
||||||
|
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Download file or folder
|
// Download file or folder
|
||||||
const handleDownload = useCallback(async (item: FileTreeNode) => {
|
const handleDownload = useCallback(async (item: FileTreeNode) => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
@@ -272,28 +286,16 @@ export function useFileTreeOperations({
|
|||||||
const downloadSingleFile = useCallback(async (item: FileTreeNode) => {
|
const downloadSingleFile = useCallback(async (item: FileTreeNode) => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
|
|
||||||
const response = await api.readFile(selectedProject.name, item.path);
|
// Use the binary streaming endpoint so downloads preserve raw bytes.
|
||||||
|
const response = await api.readFileBlob(selectedProject.name, item.path);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to download file');
|
throw new Error('Failed to download file');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const blob = await response.blob();
|
||||||
const content = data.content;
|
triggerBrowserDownload(blob, item.name);
|
||||||
|
}, [selectedProject, triggerBrowserDownload]);
|
||||||
const blob = new Blob([content], { type: 'text/plain' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = item.name;
|
|
||||||
|
|
||||||
document.body.appendChild(anchor);
|
|
||||||
anchor.click();
|
|
||||||
document.body.removeChild(anchor);
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, [selectedProject]);
|
|
||||||
|
|
||||||
// Download folder as ZIP
|
// Download folder as ZIP
|
||||||
const downloadFolderAsZip = useCallback(async (folder: FileTreeNode) => {
|
const downloadFolderAsZip = useCallback(async (folder: FileTreeNode) => {
|
||||||
@@ -306,12 +308,14 @@ export function useFileTreeOperations({
|
|||||||
const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name;
|
const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name;
|
||||||
|
|
||||||
if (node.type === 'file') {
|
if (node.type === 'file') {
|
||||||
// Fetch file content
|
const response = await api.readFileBlob(selectedProject.name, node.path);
|
||||||
const response = await api.readFile(selectedProject.name, node.path);
|
if (!response.ok) {
|
||||||
if (response.ok) {
|
throw new Error(`Failed to download "${node.name}" for ZIP export`);
|
||||||
const data = await response.json();
|
|
||||||
zip.file(fullPath, data.content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store raw bytes in the archive so binary files stay intact.
|
||||||
|
const fileBytes = await response.arrayBuffer();
|
||||||
|
zip.file(fullPath, fileBytes);
|
||||||
} else if (node.type === 'directory' && node.children) {
|
} else if (node.type === 'directory' && node.children) {
|
||||||
// Recursively process children
|
// Recursively process children
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
@@ -329,20 +333,10 @@ export function useFileTreeOperations({
|
|||||||
|
|
||||||
// Generate ZIP file
|
// Generate ZIP file
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
const url = URL.createObjectURL(zipBlob);
|
triggerBrowserDownload(zipBlob, `${folder.name}.zip`);
|
||||||
const anchor = document.createElement('a');
|
|
||||||
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = `${folder.name}.zip`;
|
|
||||||
|
|
||||||
document.body.appendChild(anchor);
|
|
||||||
anchor.click();
|
|
||||||
document.body.removeChild(anchor);
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
showToast(t('fileTree.toast.folderDownloaded', 'Folder downloaded as ZIP'), 'success');
|
showToast(t('fileTree.toast.folderDownloaded', 'Folder downloaded as ZIP'), 'success');
|
||||||
}, [selectedProject, showToast, t]);
|
}, [selectedProject, showToast, t, triggerBrowserDownload]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Rename operations
|
// Rename operations
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function BranchesView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-1 flex-col overflow-hidden ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
{/* Create branch button */}
|
{/* Create branch button */}
|
||||||
<div className="flex items-center justify-between border-b border-border/40 px-4 py-2.5">
|
<div className="flex items-center justify-between border-b border-border/40 px-4 py-2.5">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export default function ChangesView({
|
|||||||
|
|
||||||
{!gitStatus?.error && <FileStatusLegend isMobile={isMobile} />}
|
{!gitStatus?.error && <FileStatusLegend isMobile={isMobile} />}
|
||||||
|
|
||||||
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
<div className="flex-1 overflow-y-auto">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex h-32 items-center justify-center">
|
<div className="flex h-32 items-center justify-center">
|
||||||
<RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
|
<RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function HistoryView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
<div className="flex-1 overflow-y-auto">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex h-32 items-center justify-center">
|
<div className="flex h-32 items-center justify-center">
|
||||||
<RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
|
<RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Plugin } from '../../../contexts/PluginsContext';
|
|||||||
import PluginIcon from './PluginIcon';
|
import PluginIcon from './PluginIcon';
|
||||||
|
|
||||||
const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter';
|
const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter';
|
||||||
|
const TERMINAL_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-terminal';
|
||||||
|
|
||||||
/* ─── Toggle Switch ─────────────────────────────────────────────────────── */
|
/* ─── Toggle Switch ─────────────────────────────────────────────────────── */
|
||||||
function ToggleSwitch({ checked, onChange, ariaLabel }: { checked: boolean; onChange: (v: boolean) => void; ariaLabel: string }) {
|
function ToggleSwitch({ checked, onChange, ariaLabel }: { checked: boolean; onChange: (v: boolean) => void; ariaLabel: string }) {
|
||||||
@@ -264,6 +265,67 @@ function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; i
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Terminal Plugin Card ──────────────────────────────────────────────── */
|
||||||
|
function TerminalPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex overflow-hidden rounded-lg border border-dashed border-border bg-card transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500">
|
||||||
|
<div className="w-[3px] flex-shrink-0 bg-blue-500/30" />
|
||||||
|
<div className="min-w-0 flex-1 p-4">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex min-w-0 items-center gap-2.5">
|
||||||
|
<div className="h-5 w-5 flex-shrink-0 text-blue-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||||
|
<path d="M7 8l4 4-4 4"/>
|
||||||
|
<line x1="13" y1="16" x2="17" y2="16"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="text-sm font-semibold leading-none text-foreground">
|
||||||
|
{t('pluginSettings.terminalPlugin.name')}
|
||||||
|
</span>
|
||||||
|
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:bg-blue-950/50 dark:text-blue-400">
|
||||||
|
{t('pluginSettings.terminalPlugin.badge')}
|
||||||
|
</span>
|
||||||
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
|
{t('pluginSettings.tab')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
||||||
|
{t('pluginSettings.terminalPlugin.description')}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={TERMINAL_PLUGIN_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<GitBranch className="h-3 w-3" />
|
||||||
|
cloudcli-ai/cloudcli-plugin-terminal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onInstall}
|
||||||
|
disabled={installing}
|
||||||
|
className="flex flex-shrink-0 items-center gap-1.5 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{installing ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
{installing ? t('pluginSettings.installing') : t('pluginSettings.terminalPlugin.install')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Main Component ────────────────────────────────────────────────────── */
|
/* ─── Main Component ────────────────────────────────────────────────────── */
|
||||||
export default function PluginSettingsTab() {
|
export default function PluginSettingsTab() {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
@@ -273,6 +335,7 @@ export default function PluginSettingsTab() {
|
|||||||
const [gitUrl, setGitUrl] = useState('');
|
const [gitUrl, setGitUrl] = useState('');
|
||||||
const [installing, setInstalling] = useState(false);
|
const [installing, setInstalling] = useState(false);
|
||||||
const [installingStarter, setInstallingStarter] = useState(false);
|
const [installingStarter, setInstallingStarter] = useState(false);
|
||||||
|
const [installingTerminal, setInstallingTerminal] = useState(false);
|
||||||
const [installError, setInstallError] = useState<string | null>(null);
|
const [installError, setInstallError] = useState<string | null>(null);
|
||||||
const [confirmUninstall, setConfirmUninstall] = useState<string | null>(null);
|
const [confirmUninstall, setConfirmUninstall] = useState<string | null>(null);
|
||||||
const [updatingPlugins, setUpdatingPlugins] = useState<Set<string>>(new Set());
|
const [updatingPlugins, setUpdatingPlugins] = useState<Set<string>>(new Set());
|
||||||
@@ -311,6 +374,16 @@ export default function PluginSettingsTab() {
|
|||||||
setInstallingStarter(false);
|
setInstallingStarter(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInstallTerminal = async () => {
|
||||||
|
setInstallingTerminal(true);
|
||||||
|
setInstallError(null);
|
||||||
|
const result = await installPlugin(TERMINAL_PLUGIN_URL);
|
||||||
|
if (!result.success) {
|
||||||
|
setInstallError(result.error || t('pluginSettings.installFailed'));
|
||||||
|
}
|
||||||
|
setInstallingTerminal(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUninstall = async (name: string) => {
|
const handleUninstall = async (name: string) => {
|
||||||
if (confirmUninstall !== name) {
|
if (confirmUninstall !== name) {
|
||||||
setConfirmUninstall(name);
|
setConfirmUninstall(name);
|
||||||
@@ -326,6 +399,7 @@ export default function PluginSettingsTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasStarterInstalled = plugins.some((p) => p.name === 'project-stats');
|
const hasStarterInstalled = plugins.some((p) => p.name === 'project-stats');
|
||||||
|
const hasTerminalInstalled = plugins.some((p) => p.name === 'web-terminal');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -382,10 +456,17 @@ export default function PluginSettingsTab() {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Starter plugin suggestion — above the list */}
|
{/* Official plugin suggestions — above the list */}
|
||||||
{!loading && !hasStarterInstalled && (
|
{!loading && (!hasStarterInstalled || !hasTerminalInstalled) && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{!hasStarterInstalled && (
|
||||||
<StarterPluginCard onInstall={handleInstallStarter} installing={installingStarter} />
|
<StarterPluginCard onInstall={handleInstallStarter} installing={installingStarter} />
|
||||||
)}
|
)}
|
||||||
|
{!hasTerminalInstalled && (
|
||||||
|
<TerminalPluginCard onInstall={handleInstallTerminal} installing={installingTerminal} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Plugin List */}
|
{/* Plugin List */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -423,15 +504,13 @@ export default function PluginSettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Build your own */}
|
{/* Starter plugin */}
|
||||||
<div className="flex items-center justify-between gap-4 border-t border-border/50 pt-2">
|
<div className="flex items-center justify-center gap-3 border-t border-border/50 pt-2">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
|
||||||
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
|
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
|
||||||
<span className="text-xs text-muted-foreground/60">
|
<span className="text-xs text-muted-foreground/60">
|
||||||
{t('pluginSettings.buildYourOwn')}
|
{t('pluginSettings.starterPluginLabel')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span className="text-muted-foreground/20">·</span>
|
||||||
<div className="flex flex-shrink-0 items-center gap-3">
|
|
||||||
<a
|
<a
|
||||||
href={STARTER_PLUGIN_URL}
|
href={STARTER_PLUGIN_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -451,6 +530,5 @@ export default function PluginSettingsTab() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ import QuickSettingsWhisperSection from './QuickSettingsWhisperSection';
|
|||||||
|
|
||||||
type QuickSettingsContentProps = {
|
type QuickSettingsContentProps = {
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
isMobile: boolean;
|
|
||||||
preferences: QuickSettingsPreferences;
|
preferences: QuickSettingsPreferences;
|
||||||
onPreferenceChange: (key: PreferenceToggleKey, value: boolean) => void;
|
onPreferenceChange: (key: PreferenceToggleKey, value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QuickSettingsContent({
|
export default function QuickSettingsContent({
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
isMobile,
|
|
||||||
preferences,
|
preferences,
|
||||||
onPreferenceChange,
|
onPreferenceChange,
|
||||||
}: QuickSettingsContentProps) {
|
}: QuickSettingsContentProps) {
|
||||||
@@ -45,7 +43,7 @@ export default function QuickSettingsContent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-1 space-y-6 overflow-y-auto overflow-x-hidden bg-background p-4 ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
<div className="flex-1 space-y-6 overflow-y-auto overflow-x-hidden bg-background p-4">
|
||||||
<QuickSettingsSection title={t('quickSettings.sections.appearance')}>
|
<QuickSettingsSection title={t('quickSettings.sections.appearance')}>
|
||||||
<div className={SETTING_ROW_CLASS}>
|
<div className={SETTING_ROW_CLASS}>
|
||||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export default function QuickSettingsPanelView() {
|
|||||||
<QuickSettingsPanelHeader />
|
<QuickSettingsPanelHeader />
|
||||||
<QuickSettingsContent
|
<QuickSettingsContent
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
isMobile={isMobile}
|
|
||||||
preferences={quickSettingsPreferences}
|
preferences={quickSettingsPreferences}
|
||||||
onPreferenceChange={handlePreferenceChange}
|
onPreferenceChange={handlePreferenceChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins';
|
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about';
|
||||||
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||||
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
||||||
export type ProjectSortOrder = 'name' | 'date';
|
export type ProjectSortOrder = 'name' | 'date';
|
||||||
|
|||||||
46
src/components/settings/view/PremiumFeatureCard.tsx
Normal file
46
src/components/settings/view/PremiumFeatureCard.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ExternalLink, Lock } from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const CLOUDCLI_URL = 'https://cloudcli.ai';
|
||||||
|
|
||||||
|
type PremiumFeatureCardProps = {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
ctaText?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PremiumFeatureCard({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ctaText = 'Available with CloudCLI Pro',
|
||||||
|
}: PremiumFeatureCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 p-5">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="text-sm font-medium text-foreground">{title}</h4>
|
||||||
|
<Lock className="h-3 w-3 text-muted-foreground/60" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={CLOUDCLI_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
||||||
|
>
|
||||||
|
{ctaText}
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab';
|
|||||||
import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab';
|
import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab';
|
||||||
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
||||||
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
||||||
|
import AboutTab from '../view/tabs/AboutTab';
|
||||||
import { useSettingsController } from '../hooks/useSettingsController';
|
import { useSettingsController } from '../hooks/useSettingsController';
|
||||||
import { useWebPush } from '../../../hooks/useWebPush';
|
import { useWebPush } from '../../../hooks/useWebPush';
|
||||||
import type { SettingsProps } from '../types/types';
|
import type { SettingsProps } from '../types/types';
|
||||||
@@ -206,6 +207,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
{activeTab === 'api' && <CredentialsSettingsTab />}
|
{activeTab === 'api' && <CredentialsSettingsTab />}
|
||||||
|
|
||||||
{activeTab === 'plugins' && <PluginSettingsTab />}
|
{activeTab === 'plugins' && <PluginSettingsTab />}
|
||||||
|
|
||||||
|
{activeTab === 'about' && <AboutTab />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GitBranch, Key, Puzzle } from 'lucide-react';
|
import { GitBranch, Info, Key, Puzzle } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { SettingsMainTab } from '../types/types';
|
import type { SettingsMainTab } from '../types/types';
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ const TAB_CONFIG: MainTabConfig[] = [
|
|||||||
{ id: 'tasks', labelKey: 'mainTabs.tasks' },
|
{ id: 'tasks', labelKey: 'mainTabs.tasks' },
|
||||||
{ id: 'notifications', labelKey: 'mainTabs.notifications' },
|
{ id: 'notifications', labelKey: 'mainTabs.notifications' },
|
||||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||||
|
{ id: 'about', labelKey: 'mainTabs.about', icon: Info },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) {
|
export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bell, Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
import { Bell, Bot, GitBranch, Info, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '../../../lib/utils';
|
import { cn } from '../../../lib/utils';
|
||||||
import { PillBar, Pill } from '../../../shared/view/ui';
|
import { PillBar, Pill } from '../../../shared/view/ui';
|
||||||
@@ -23,6 +23,7 @@ const NAV_ITEMS: NavItem[] = [
|
|||||||
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
||||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||||
{ id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell },
|
{ id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell },
|
||||||
|
{ id: 'about', labelKey: 'mainTabs.about', icon: Info },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
||||||
|
|||||||
166
src/components/settings/view/tabs/AboutTab.tsx
Normal file
166
src/components/settings/view/tabs/AboutTab.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { ExternalLink, MessageSquare, Star } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IS_PLATFORM } from '../../../../constants/config';
|
||||||
|
import { useVersionCheck } from '../../../../hooks/useVersionCheck';
|
||||||
|
import PremiumFeatureCard from '../PremiumFeatureCard';
|
||||||
|
import { Cloud, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
||||||
|
const DISCORD_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
|
const DOCS_URL = 'https://cloudcli.ai/docs/plugin-overview';
|
||||||
|
const CLOUDCLI_URL = 'https://cloudcli.ai';
|
||||||
|
|
||||||
|
function GitHubIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AboutTab() {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
|
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
|
||||||
|
const releasesUrl = releaseInfo?.htmlUrl || `${GITHUB_REPO_URL}/releases`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Logo + name + version */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-primary/90 shadow-sm">
|
||||||
|
<MessageSquare className="h-5 w-5 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-base font-semibold text-foreground">CloudCLI</span>
|
||||||
|
<a
|
||||||
|
href={releasesUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
v{currentVersion}
|
||||||
|
</a>
|
||||||
|
{updateAvailable && latestVersion && (
|
||||||
|
<a
|
||||||
|
href={releasesUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 rounded-full bg-green-500/10 px-2 py-0.5 text-[10px] font-medium text-green-600 transition-colors hover:bg-green-500/20 dark:text-green-400"
|
||||||
|
>
|
||||||
|
{t('apiKeys.version.updateAvailable', { version: latestVersion })}
|
||||||
|
<ExternalLink className="h-2.5 w-2.5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||||
|
Open-source AI coding assistant interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Star on GitHub button */}
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg border border-border/60 bg-background px-3.5 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
|
<Star className="h-3.5 w-3.5" />
|
||||||
|
<span>Star on GitHub</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
<div className="flex flex-wrap gap-4 text-sm">
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={DISCORD_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<DiscordIcon className="h-4 w-4" />
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={DOCS_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
|
Docs
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={CLOUDCLI_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
|
cloudcli.ai
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hosted CTA (OSS mode only) */}
|
||||||
|
{!IS_PLATFORM && (
|
||||||
|
<div className="rounded-xl border border-primary/10 bg-primary/5 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-foreground">Try CloudCLI Hosted</h4>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Team collaboration, shared MCP configs, settings sync across environments, and managed infrastructure.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={CLOUDCLI_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Premium feature placeholders (OSS mode only) */}
|
||||||
|
{!IS_PLATFORM && (
|
||||||
|
<div className="space-y-4 border-t border-border/50 pt-6">
|
||||||
|
<h3 className="text-sm font-medium text-foreground">CloudCLI Pro Features</h3>
|
||||||
|
<PremiumFeatureCard
|
||||||
|
icon={<Cloud className="h-5 w-5" />}
|
||||||
|
title="Sync Settings"
|
||||||
|
description="Keep your preferences, MCP configs, and theme in sync across all your environments."
|
||||||
|
/>
|
||||||
|
<PremiumFeatureCard
|
||||||
|
icon={<Users className="h-5 w-5" />}
|
||||||
|
title="Team Management"
|
||||||
|
description="Multiple users, role-based access, and shared projects for your team."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* License */}
|
||||||
|
<div className="border-t border-border/50 pt-4">
|
||||||
|
<p className="text-xs text-muted-foreground/60">
|
||||||
|
Licensed under AGPL-3.0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Edit3, Globe, Plus, Server, Terminal, Trash2, Zap } from 'lucide-react';
|
import { Edit3, Globe, Plus, Server, Terminal, Trash2, Users, Zap } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Badge, Button } from '../../../../../../../shared/view/ui';
|
import { Badge, Button } from '../../../../../../../shared/view/ui';
|
||||||
|
import { IS_PLATFORM } from '../../../../../../../constants/config';
|
||||||
|
import PremiumFeatureCard from '../../../../PremiumFeatureCard';
|
||||||
import type { McpServer, McpToolsResult, McpTestResult } from '../../../../../types/types';
|
import type { McpServer, McpToolsResult, McpTestResult } from '../../../../../types/types';
|
||||||
|
|
||||||
const getTransportIcon = (type: string | undefined) => {
|
const getTransportIcon = (type: string | undefined) => {
|
||||||
@@ -179,6 +181,14 @@ function ClaudeMcpServers({
|
|||||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!IS_PLATFORM && (
|
||||||
|
<PremiumFeatureCard
|
||||||
|
icon={<Users className="h-5 w-5" />}
|
||||||
|
title="Team MCP Configs"
|
||||||
|
description="Share MCP server configurations across your team. Everyone stays in sync automatically."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useVersionCheck } from '../../../../../hooks/useVersionCheck';
|
|
||||||
import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings';
|
import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings';
|
||||||
import ApiKeysSection from './sections/ApiKeysSection';
|
import ApiKeysSection from './sections/ApiKeysSection';
|
||||||
import GithubCredentialsSection from './sections/GithubCredentialsSection';
|
import GithubCredentialsSection from './sections/GithubCredentialsSection';
|
||||||
import NewApiKeyAlert from './sections/NewApiKeyAlert';
|
import NewApiKeyAlert from './sections/NewApiKeyAlert';
|
||||||
import VersionInfoSection from './sections/VersionInfoSection';
|
|
||||||
|
|
||||||
export default function CredentialsSettingsTab() {
|
export default function CredentialsSettingsTab() {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
|
|
||||||
const {
|
const {
|
||||||
apiKeys,
|
apiKeys,
|
||||||
githubCredentials,
|
githubCredentials,
|
||||||
@@ -89,12 +86,6 @@ export default function CredentialsSettingsTab() {
|
|||||||
onDeleteGithubCredential={deleteGithubCredential}
|
onDeleteGithubCredential={deleteGithubCredential}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VersionInfoSection
|
|
||||||
currentVersion={currentVersion}
|
|
||||||
updateAvailable={updateAvailable}
|
|
||||||
latestVersion={latestVersion}
|
|
||||||
releaseInfo={releaseInfo}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,29 @@
|
|||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink, Star, MessageSquare } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IS_PLATFORM } from '../../../../../../constants/config';
|
||||||
import type { ReleaseInfo } from '../../../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../../../types/sharedTypes';
|
||||||
|
|
||||||
|
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
||||||
|
const DISCORD_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
|
const DOCS_URL = 'https://cloudcli.ai/docs/plugin-overview';
|
||||||
|
const CLOUDCLI_URL = 'https://cloudcli.ai';
|
||||||
|
|
||||||
|
function GitHubIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type VersionInfoSectionProps = {
|
type VersionInfoSectionProps = {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
@@ -16,16 +38,25 @@ export default function VersionInfoSection({
|
|||||||
releaseInfo,
|
releaseInfo,
|
||||||
}: VersionInfoSectionProps) {
|
}: VersionInfoSectionProps) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const releasesUrl = releaseInfo?.htmlUrl || 'https://github.com/siteboon/claudecodeui/releases';
|
const releasesUrl = releaseInfo?.htmlUrl || `${GITHUB_REPO_URL}/releases`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-border/50 pt-6">
|
<div className="border-t border-border/50 pt-6">
|
||||||
<div className="flex items-center justify-between text-xs italic text-muted-foreground/60">
|
{/* About CloudCLI */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Logo + name + version */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-primary/90 shadow-sm">
|
||||||
|
<MessageSquare className="h-4.5 w-4.5 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-semibold text-foreground">CloudCLI</span>
|
||||||
<a
|
<a
|
||||||
href={releasesUrl}
|
href={releasesUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="transition-colors hover:text-muted-foreground"
|
className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
v{currentVersion}
|
v{currentVersion}
|
||||||
</a>
|
</a>
|
||||||
@@ -34,13 +65,90 @@ export default function VersionInfoSection({
|
|||||||
href={releasesUrl}
|
href={releasesUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1.5 rounded-full bg-green-500/10 px-2 py-0.5 font-medium not-italic text-green-600 transition-colors hover:bg-green-500/20 dark:text-green-400"
|
className="flex items-center gap-1 rounded-full bg-green-500/10 px-2 py-0.5 text-[10px] font-medium text-green-600 transition-colors hover:bg-green-500/20 dark:text-green-400"
|
||||||
>
|
>
|
||||||
<span className="text-[10px]">{t('apiKeys.version.updateAvailable', { version: latestVersion })}</span>
|
{t('apiKeys.version.updateAvailable', { version: latestVersion })}
|
||||||
<ExternalLink className="h-2.5 w-2.5" />
|
<ExternalLink className="h-2.5 w-2.5" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
Open-source AI coding assistant interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Star on GitHub button */}
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
|
<Star className="h-3.5 w-3.5" />
|
||||||
|
<span>Star on GitHub</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
<div className="flex flex-wrap gap-3 text-xs">
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-3.5 w-3.5" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={DISCORD_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<DiscordIcon className="h-3.5 w-3.5" />
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={DOCS_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
Docs
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={CLOUDCLI_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
cloudcli.ai
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hosted CTA (OSS mode only) */}
|
||||||
|
{!IS_PLATFORM && (
|
||||||
|
<div className="rounded-xl border border-primary/10 bg-primary/5 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-foreground">Try CloudCLI Hosted</h4>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Team collaboration, shared MCP configs, settings sync across environments, and managed infrastructure.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={CLOUDCLI_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function TerminalShortcutsPanel({
|
|||||||
wsRef,
|
wsRef,
|
||||||
terminalRef,
|
terminalRef,
|
||||||
isConnected,
|
isConnected,
|
||||||
bottomOffset = 'bottom-14',
|
bottomOffset = 'bottom-0',
|
||||||
}: TerminalShortcutsPanelProps) {
|
}: TerminalShortcutsPanelProps) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const [ctrlActive, setCtrlActive] = useState(false);
|
const [ctrlActive, setCtrlActive] = useState(false);
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ function Sidebar({
|
|||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
|
currentVersion={currentVersion}
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
projectListProps={projectListProps}
|
projectListProps={projectListProps}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Star, X } from 'lucide-react';
|
||||||
|
import { useGitHubStars } from '../../../../hooks/useGitHubStars';
|
||||||
|
import { IS_PLATFORM } from '../../../../constants/config';
|
||||||
|
|
||||||
|
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
||||||
|
|
||||||
|
function GitHubIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GitHubStarBadge() {
|
||||||
|
const { formattedCount, isDismissed, dismiss } = useGitHubStars('siteboon', 'claudecodeui');
|
||||||
|
|
||||||
|
if (IS_PLATFORM || isDismissed) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group/star relative hidden md:block">
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-lg border border-border/50 bg-muted/30 px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-3.5 w-3.5" />
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
<span className="font-medium">Star</span>
|
||||||
|
{formattedCount && (
|
||||||
|
<span className="border-l border-border/50 pl-1.5 tabular-nums">{formattedCount}</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dismiss();
|
||||||
|
}}
|
||||||
|
className="absolute -right-1.5 -top-1.5 hidden h-4 w-4 items-center justify-center rounded-full border border-border/50 bg-muted text-muted-foreground transition-colors hover:text-foreground group-hover/star:flex"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<X className="h-2.5 w-2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Settings, Sparkles, PanelLeftOpen } from 'lucide-react';
|
import { Settings, Sparkles, PanelLeftOpen, Bug } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
|
||||||
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
|
const GITHUB_ISSUES_URL = 'https://github.com/siteboon/claudecodeui/issues/new';
|
||||||
|
|
||||||
function DiscordIcon({ className }: { className?: string }) {
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -50,6 +51,18 @@ export default function SidebarCollapsed({
|
|||||||
<Settings className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
<Settings className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Report Issue */}
|
||||||
|
<a
|
||||||
|
href={GITHUB_ISSUES_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex h-8 w-8 items-center justify-center rounded-lg transition-colors hover:bg-accent/80"
|
||||||
|
aria-label={t('actions.reportIssue')}
|
||||||
|
title={t('actions.reportIssue')}
|
||||||
|
>
|
||||||
|
<Bug className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Discord */}
|
{/* Discord */}
|
||||||
<a
|
<a
|
||||||
href={DISCORD_INVITE_URL}
|
href={DISCORD_INVITE_URL}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ type SidebarContentProps = {
|
|||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
|
currentVersion: string;
|
||||||
onShowVersionModal: () => void;
|
onShowVersionModal: () => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
projectListProps: SidebarProjectListProps;
|
projectListProps: SidebarProjectListProps;
|
||||||
@@ -83,6 +84,7 @@ export default function SidebarContent({
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
|
currentVersion,
|
||||||
onShowVersionModal,
|
onShowVersionModal,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
projectListProps,
|
projectListProps,
|
||||||
@@ -217,6 +219,7 @@ export default function SidebarContent({
|
|||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
|
currentVersion={currentVersion}
|
||||||
onShowVersionModal={onShowVersionModal}
|
onShowVersionModal={onShowVersionModal}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Settings, ArrowUpCircle } from 'lucide-react';
|
import { Settings, ArrowUpCircle, Bug } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
import { IS_PLATFORM } from '../../../../constants/config';
|
||||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||||
|
|
||||||
|
const GITHUB_ISSUES_URL = 'https://github.com/siteboon/claudecodeui/issues/new';
|
||||||
|
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
||||||
|
|
||||||
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
|
|
||||||
function DiscordIcon({ className }: { className?: string }) {
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
@@ -16,6 +20,7 @@ type SidebarFooterProps = {
|
|||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
|
currentVersion: string;
|
||||||
onShowVersionModal: () => void;
|
onShowVersionModal: () => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
@@ -25,6 +30,7 @@ export default function SidebarFooter({
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
|
currentVersion,
|
||||||
onShowVersionModal,
|
onShowVersionModal,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
t,
|
t,
|
||||||
@@ -79,11 +85,24 @@ export default function SidebarFooter({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Discord + Settings */}
|
{/* Community + Settings */}
|
||||||
<div className="nav-divider" />
|
<div className="nav-divider" />
|
||||||
|
|
||||||
{/* Desktop Discord */}
|
{/* Desktop Report Issue */}
|
||||||
<div className="hidden px-2 pt-1.5 md:block">
|
<div className="hidden px-2 pt-1.5 md:block">
|
||||||
|
<a
|
||||||
|
href={GITHUB_ISSUES_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Bug className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-sm">{t('actions.reportIssue')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Discord */}
|
||||||
|
<div className="hidden px-2 md:block">
|
||||||
<a
|
<a
|
||||||
href={DISCORD_INVITE_URL}
|
href={DISCORD_INVITE_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -106,8 +125,37 @@ export default function SidebarFooter({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Discord */}
|
{/* Desktop version brand line (OSS mode only) */}
|
||||||
|
{!IS_PLATFORM && (
|
||||||
|
<div className="hidden px-3 py-2 text-center md:block">
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[10px] text-muted-foreground/40 transition-colors hover:text-muted-foreground"
|
||||||
|
>
|
||||||
|
CloudCLI v{currentVersion} – {t('branding.openSource')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mobile Report Issue */}
|
||||||
<div className="px-3 pt-3 md:hidden">
|
<div className="px-3 pt-3 md:hidden">
|
||||||
|
<a
|
||||||
|
href={GITHUB_ISSUES_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80">
|
||||||
|
<Bug className="w-4.5 h-4.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<span className="text-base font-medium text-foreground">{t('actions.reportIssue')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Discord */}
|
||||||
|
<div className="px-3 pt-2 md:hidden">
|
||||||
<a
|
<a
|
||||||
href={DISCORD_INVITE_URL}
|
href={DISCORD_INVITE_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -122,7 +170,7 @@ export default function SidebarFooter({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile settings */}
|
{/* Mobile settings */}
|
||||||
<div className="px-3 pb-20 pt-2 md:hidden">
|
<div className="px-3 pb-3 pt-2 md:hidden">
|
||||||
<button
|
<button
|
||||||
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]"
|
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]"
|
||||||
onClick={onShowSettings}
|
onClick={onShowSettings}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { TFunction } from 'i18next';
|
|||||||
import { Button, Input } from '../../../../shared/view/ui';
|
import { Button, Input } from '../../../../shared/view/ui';
|
||||||
import { IS_PLATFORM } from '../../../../constants/config';
|
import { IS_PLATFORM } from '../../../../constants/config';
|
||||||
import { cn } from '../../../../lib/utils';
|
import { cn } from '../../../../lib/utils';
|
||||||
|
import GitHubStarBadge from './GitHubStarBadge';
|
||||||
|
|
||||||
type SearchMode = 'projects' | 'conversations';
|
type SearchMode = 'projects' | 'conversations';
|
||||||
|
|
||||||
@@ -106,6 +107,8 @@ export default function SidebarHeader({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<GitHubStarBadge />
|
||||||
|
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
{projectsCount > 0 && !isLoading && (
|
{projectsCount > 0 && !isLoading && (
|
||||||
<div className="mt-2.5 space-y-2">
|
<div className="mt-2.5 space-y-2">
|
||||||
|
|||||||
@@ -80,6 +80,29 @@ export default function SidebarProjectSessions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-3 space-y-1 border-l border-border pl-3">
|
<div className="ml-3 space-y-1 border-l border-border pl-3">
|
||||||
|
<div className="px-3 pb-1 pt-1 md:hidden">
|
||||||
|
<button
|
||||||
|
className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-primary text-xs font-medium text-primary-foreground transition-all duration-150 hover:bg-primary/90 active:scale-[0.98]"
|
||||||
|
onClick={() => {
|
||||||
|
onProjectSelect(project);
|
||||||
|
onNewSession(project);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
{t('sessions.newSession')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="hidden h-8 w-full justify-start gap-2 bg-primary text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 md:flex"
|
||||||
|
onClick={() => onNewSession(project)}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
{t('sessions.newSession')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{!initialSessionsLoaded ? (
|
{!initialSessionsLoaded ? (
|
||||||
<SessionListSkeleton />
|
<SessionListSkeleton />
|
||||||
) : !hasSessions && !isLoadingSessions ? (
|
) : !hasSessions && !isLoadingSessions ? (
|
||||||
@@ -129,29 +152,6 @@ export default function SidebarProjectSessions({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="px-3 pb-2 md:hidden">
|
|
||||||
<button
|
|
||||||
className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-primary text-xs font-medium text-primary-foreground transition-all duration-150 hover:bg-primary/90 active:scale-[0.98]"
|
|
||||||
onClick={() => {
|
|
||||||
onProjectSelect(project);
|
|
||||||
onNewSession(project);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
{t('sessions.newSession')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="mt-1 hidden h-8 w-full justify-start gap-2 bg-primary text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 md:flex"
|
|
||||||
onClick={() => onNewSession(project)}
|
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
{t('sessions.newSession')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/hooks/useGitHubStars.ts
Normal file
77
src/hooks/useGitHubStars.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
const CACHE_KEY = 'CLOUDCLI_GITHUB_STARS';
|
||||||
|
const DISMISS_KEY = 'CLOUDCLI_HIDE_GITHUB_STAR';
|
||||||
|
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
type CachedStars = {
|
||||||
|
count: number;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGitHubStars = (owner: string, repo: string) => {
|
||||||
|
const [starCount, setStarCount] = useState<number | null>(null);
|
||||||
|
const [isDismissed, setIsDismissed] = useState(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(DISMISS_KEY) === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDismissed) return;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(CACHE_KEY);
|
||||||
|
if (cached) {
|
||||||
|
const parsed: CachedStars = JSON.parse(cached);
|
||||||
|
if (Date.now() - parsed.timestamp < CACHE_TTL) {
|
||||||
|
setStarCount(parsed.count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStars = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
||||||
|
if (!response.ok) return;
|
||||||
|
const data = await response.json();
|
||||||
|
const count = data.stargazers_count;
|
||||||
|
if (typeof count === 'number') {
|
||||||
|
setStarCount(count);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CACHE_KEY, JSON.stringify({ count, timestamp: Date.now() }));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silent fail
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void fetchStars();
|
||||||
|
}, [owner, repo, isDismissed]);
|
||||||
|
|
||||||
|
const dismiss = useCallback(() => {
|
||||||
|
setIsDismissed(true);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(DISMISS_KEY, 'true');
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formattedCount = starCount !== null
|
||||||
|
? starCount >= 1000
|
||||||
|
? `${(starCount / 1000).toFixed(1)}k`
|
||||||
|
: `${starCount}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { starCount, formattedCount, isDismissed, dismiss };
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Willkommen zurück",
|
"title": "Willkommen zurück",
|
||||||
"description": "Meld dich bei deinem Claude Code UI-Konto an",
|
"description": "Meld dich bei deinem CloudCLI-Konto an",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"submit": "Anmelden",
|
"submit": "Anmelden",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "Im Editor öffnen"
|
"openInEditor": "Im Editor öffnen"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Claude Code UI wird geladen",
|
"loading": "CloudCLI wird geladen",
|
||||||
"settingUpWorkspace": "Arbeitsbereich wird eingerichtet...",
|
"settingUpWorkspace": "Arbeitsbereich wird eingerichtet...",
|
||||||
"chooseProject": "Projekt auswählen",
|
"chooseProject": "Projekt auswählen",
|
||||||
"selectProjectDescription": "Wähl ein Projekt aus der Seitenleiste, um mit Claude zu programmieren. Jedes Projekt enthält deine Chat-Sitzungen und den Dateiverlauf.",
|
"selectProjectDescription": "Wähl ein Projekt aus der Seitenleiste, um mit Claude zu programmieren. Jedes Projekt enthält deine Chat-Sitzungen und den Dateiverlauf.",
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
"viewFullRelease": "Vollständige Version anzeigen",
|
"viewFullRelease": "Vollständige Version anzeigen",
|
||||||
"updateProgress": "Update-Fortschritt:",
|
"updateProgress": "Update-Fortschritt:",
|
||||||
"manualUpgrade": "Manuelles Upgrade:",
|
"manualUpgrade": "Manuelles Upgrade:",
|
||||||
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
|
||||||
"manualUpgradeHint": "Oder klick auf \"Jetzt aktualisieren\", um das Update automatisch durchzuführen.",
|
"manualUpgradeHint": "Oder klick auf \"Jetzt aktualisieren\", um das Update automatisch durchzuführen.",
|
||||||
"updateCompleted": "Update erfolgreich abgeschlossen!",
|
"updateCompleted": "Update erfolgreich abgeschlossen!",
|
||||||
"restartServer": "Bitte starte den Server neu, um die Änderungen anzuwenden.",
|
"restartServer": "Bitte starte den Server neu, um die Änderungen anzuwenden.",
|
||||||
|
|||||||
@@ -105,7 +105,8 @@
|
|||||||
"git": "Git",
|
"git": "Git",
|
||||||
"apiTokens": "API & Token",
|
"apiTokens": "API & Token",
|
||||||
"tasks": "Aufgaben",
|
"tasks": "Aufgaben",
|
||||||
"plugins": "Plugins"
|
"plugins": "Plugins",
|
||||||
|
"about": "Info"
|
||||||
},
|
},
|
||||||
"appearanceSettings": {
|
"appearanceSettings": {
|
||||||
"darkMode": {
|
"darkMode": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "Führ Claude CLI in einem Projektverzeichnis aus, um zu beginnen"
|
"runClaudeCli": "Führ Claude CLI in einem Projektverzeichnis aus, um zu beginnen"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code UI",
|
"title": "CloudCLI",
|
||||||
"subtitle": "KI-Programmierassistent-Oberfläche"
|
"subtitle": "KI-Programmierassistent-Oberfläche"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,7 +65,12 @@
|
|||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"joinCommunity": "Community beitreten"
|
"joinCommunity": "Community beitreten",
|
||||||
|
"reportIssue": "Problem melden",
|
||||||
|
"starOnGithub": "Stern auf GitHub"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Welcome Back",
|
"title": "Welcome Back",
|
||||||
"description": "Sign in to your Claude Code UI account",
|
"description": "Sign in to your CloudCLI self-hosted account",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"submit": "Sign In",
|
"submit": "Sign In",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "Open in Editor"
|
"openInEditor": "Open in Editor"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Loading Claude Code UI",
|
"loading": "Loading CloudCLI",
|
||||||
"settingUpWorkspace": "Setting up your workspace...",
|
"settingUpWorkspace": "Setting up your workspace...",
|
||||||
"chooseProject": "Choose Your Project",
|
"chooseProject": "Choose Your Project",
|
||||||
"selectProjectDescription": "Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.",
|
"selectProjectDescription": "Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.",
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
"viewFullRelease": "View full release",
|
"viewFullRelease": "View full release",
|
||||||
"updateProgress": "Update Progress:",
|
"updateProgress": "Update Progress:",
|
||||||
"manualUpgrade": "Manual upgrade:",
|
"manualUpgrade": "Manual upgrade:",
|
||||||
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
|
||||||
"manualUpgradeHint": "Or click \"Update Now\" to run the update automatically.",
|
"manualUpgradeHint": "Or click \"Update Now\" to run the update automatically.",
|
||||||
"updateCompleted": "Update completed successfully!",
|
"updateCompleted": "Update completed successfully!",
|
||||||
"restartServer": "Please restart the server to apply changes.",
|
"restartServer": "Please restart the server to apply changes.",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API & Tokens",
|
"apiTokens": "API & Tokens",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"plugins": "Plugins"
|
"plugins": "Plugins",
|
||||||
|
"about": "About"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
"installFailed": "Installation failed",
|
"installFailed": "Installation failed",
|
||||||
"uninstallFailed": "Uninstall failed",
|
"uninstallFailed": "Uninstall failed",
|
||||||
"toggleFailed": "Toggle failed",
|
"toggleFailed": "Toggle failed",
|
||||||
"buildYourOwn": "Build your own plugin",
|
"starterPluginLabel": "Starter Plugin",
|
||||||
"starter": "Starter",
|
"starter": "Starter",
|
||||||
"docs": "Docs",
|
"docs": "Docs",
|
||||||
"starterPlugin": {
|
"starterPlugin": {
|
||||||
@@ -485,6 +485,12 @@
|
|||||||
"description": "File counts, lines of code, file-type breakdown, and recent activity for your project.",
|
"description": "File counts, lines of code, file-type breakdown, and recent activity for your project.",
|
||||||
"install": "Install"
|
"install": "Install"
|
||||||
},
|
},
|
||||||
|
"terminalPlugin": {
|
||||||
|
"name": "Terminal",
|
||||||
|
"badge": "official",
|
||||||
|
"description": "Integrated terminal with full shell access directly within the interface.",
|
||||||
|
"install": "Install"
|
||||||
|
},
|
||||||
"morePlugins": "More",
|
"morePlugins": "More",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "Run Claude CLI in a project directory to get started"
|
"runClaudeCli": "Run Claude CLI in a project directory to get started"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code UI",
|
"title": "CloudCLI",
|
||||||
"subtitle": "AI coding assistant interface"
|
"subtitle": "AI coding assistant interface"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,7 +65,12 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"joinCommunity": "Join Community"
|
"joinCommunity": "Join Community",
|
||||||
|
"reportIssue": "Report Issue",
|
||||||
|
"starOnGithub": "Star on GitHub"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "おかえりなさい",
|
"title": "おかえりなさい",
|
||||||
"description": "Claude Code UIアカウントにサインイン",
|
"description": "CloudCLIアカウントにサインイン",
|
||||||
"username": "ユーザー名",
|
"username": "ユーザー名",
|
||||||
"password": "パスワード",
|
"password": "パスワード",
|
||||||
"submit": "サインイン",
|
"submit": "サインイン",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "エディタで開く"
|
"openInEditor": "エディタで開く"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Claude Code UI を読み込んでいます",
|
"loading": "CloudCLI を読み込んでいます",
|
||||||
"settingUpWorkspace": "ワークスペースを準備しています...",
|
"settingUpWorkspace": "ワークスペースを準備しています...",
|
||||||
"chooseProject": "プロジェクトを選択",
|
"chooseProject": "プロジェクトを選択",
|
||||||
"selectProjectDescription": "サイドバーからプロジェクトを選択して、Claudeとコーディングを始めましょう。各プロジェクトにはチャットセッションとファイル履歴が含まれています。",
|
"selectProjectDescription": "サイドバーからプロジェクトを選択して、Claudeとコーディングを始めましょう。各プロジェクトにはチャットセッションとファイル履歴が含まれています。",
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
"viewFullRelease": "リリース全文を見る",
|
"viewFullRelease": "リリース全文を見る",
|
||||||
"updateProgress": "アップデートの進捗:",
|
"updateProgress": "アップデートの進捗:",
|
||||||
"manualUpgrade": "手動アップグレード:",
|
"manualUpgrade": "手動アップグレード:",
|
||||||
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
|
||||||
"manualUpgradeHint": "または「今すぐ更新」をクリックして自動的にアップデートを実行できます。",
|
"manualUpgradeHint": "または「今すぐ更新」をクリックして自動的にアップデートを実行できます。",
|
||||||
"updateCompleted": "アップデートが完了しました!",
|
"updateCompleted": "アップデートが完了しました!",
|
||||||
"restartServer": "変更を適用するにはサーバーを再起動してください。",
|
"restartServer": "変更を適用するにはサーバーを再起動してください。",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API & トークン",
|
"apiTokens": "API & トークン",
|
||||||
"tasks": "タスク",
|
"tasks": "タスク",
|
||||||
"notifications": "通知",
|
"notifications": "通知",
|
||||||
"plugins": "プラグイン"
|
"plugins": "プラグイン",
|
||||||
|
"about": "概要"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "通知",
|
"title": "通知",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "プロジェクトディレクトリでClaude CLIを実行して始めましょう"
|
"runClaudeCli": "プロジェクトディレクトリでClaude CLIを実行して始めましょう"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code UI",
|
"title": "CloudCLI",
|
||||||
"subtitle": "AIコーディングアシスタント"
|
"subtitle": "AIコーディングアシスタント"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -64,7 +64,12 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"rename": "名前の変更",
|
"rename": "名前の変更",
|
||||||
"joinCommunity": "コミュニティに参加"
|
"joinCommunity": "コミュニティに参加",
|
||||||
|
"reportIssue": "問題を報告",
|
||||||
|
"starOnGithub": "GitHubでスター"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"openSource": "オープンソース"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "アクティブ",
|
"active": "アクティブ",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "다시 오신 것을 환영합니다",
|
"title": "다시 오신 것을 환영합니다",
|
||||||
"description": "Claude Code UI 계정에 로그인하세요",
|
"description": "CloudCLI 계정에 로그인하세요",
|
||||||
"username": "사용자명",
|
"username": "사용자명",
|
||||||
"password": "비밀번호",
|
"password": "비밀번호",
|
||||||
"submit": "로그인",
|
"submit": "로그인",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "에디터에서 열기"
|
"openInEditor": "에디터에서 열기"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Claude Code UI 로딩 중",
|
"loading": "CloudCLI 로딩 중",
|
||||||
"settingUpWorkspace": "워크스페이스 설정 중...",
|
"settingUpWorkspace": "워크스페이스 설정 중...",
|
||||||
"chooseProject": "프로젝트 선택",
|
"chooseProject": "프로젝트 선택",
|
||||||
"selectProjectDescription": "사이드바에서 프로젝트를 선택하여 Claude와 코딩을 시작하세요. 각 프로젝트에는 채팅 세션과 파일 히스토리가 포함됩니다.",
|
"selectProjectDescription": "사이드바에서 프로젝트를 선택하여 Claude와 코딩을 시작하세요. 각 프로젝트에는 채팅 세션과 파일 히스토리가 포함됩니다.",
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
"viewFullRelease": "전체 릴리스 보기",
|
"viewFullRelease": "전체 릴리스 보기",
|
||||||
"updateProgress": "업데이트 진행 상황:",
|
"updateProgress": "업데이트 진행 상황:",
|
||||||
"manualUpgrade": "수동 업그레이드:",
|
"manualUpgrade": "수동 업그레이드:",
|
||||||
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
|
||||||
"manualUpgradeHint": "또는 \"지금 업데이트\"를 클릭하여 자동으로 업데이트합니다.",
|
"manualUpgradeHint": "또는 \"지금 업데이트\"를 클릭하여 자동으로 업데이트합니다.",
|
||||||
"updateCompleted": "업데이트가 완료되었습니다!",
|
"updateCompleted": "업데이트가 완료되었습니다!",
|
||||||
"restartServer": "변경사항을 적용하려면 서버를 재시작하세요.",
|
"restartServer": "변경사항을 적용하려면 서버를 재시작하세요.",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API & 토큰",
|
"apiTokens": "API & 토큰",
|
||||||
"tasks": "작업",
|
"tasks": "작업",
|
||||||
"notifications": "알림",
|
"notifications": "알림",
|
||||||
"plugins": "플러그인"
|
"plugins": "플러그인",
|
||||||
|
"about": "정보"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "알림",
|
"title": "알림",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "프로젝트 디렉토리에서 Claude CLI를 실행하여 시작하세요"
|
"runClaudeCli": "프로젝트 디렉토리에서 Claude CLI를 실행하여 시작하세요"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code UI",
|
"title": "CloudCLI",
|
||||||
"subtitle": "AI 코딩 어시스턴트 UI"
|
"subtitle": "AI 코딩 어시스턴트 UI"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -64,7 +64,12 @@
|
|||||||
"save": "저장",
|
"save": "저장",
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"rename": "이름 변경",
|
"rename": "이름 변경",
|
||||||
"joinCommunity": "커뮤니티 참여"
|
"joinCommunity": "커뮤니티 참여",
|
||||||
|
"reportIssue": "문제 신고",
|
||||||
|
"starOnGithub": "GitHub에서 스타"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"openSource": "오픈 소스"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "활성",
|
"active": "활성",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Добро пожаловать",
|
"title": "Добро пожаловать",
|
||||||
"description": "Войдите в свой аккаунт Claude Code UI",
|
"description": "Войдите в свой аккаунт CloudCLI",
|
||||||
"username": "Имя пользователя",
|
"username": "Имя пользователя",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"submit": "Войти",
|
"submit": "Войти",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "Открыть в редакторе"
|
"openInEditor": "Открыть в редакторе"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Загрузка Claude Code UI",
|
"loading": "Загрузка CloudCLI",
|
||||||
"settingUpWorkspace": "Настройка рабочего пространства...",
|
"settingUpWorkspace": "Настройка рабочего пространства...",
|
||||||
"chooseProject": "Выберите проект",
|
"chooseProject": "Выберите проект",
|
||||||
"selectProjectDescription": "Выберите проект на боковой панели, чтобы начать работу с Claude. Каждый проект содержит ваши сеансы чата и историю файлов.",
|
"selectProjectDescription": "Выберите проект на боковой панели, чтобы начать работу с Claude. Каждый проект содержит ваши сеансы чата и историю файлов.",
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
"viewFullRelease": "Посмотреть полный релиз",
|
"viewFullRelease": "Посмотреть полный релиз",
|
||||||
"updateProgress": "Прогресс обновления:",
|
"updateProgress": "Прогресс обновления:",
|
||||||
"manualUpgrade": "Ручное обновление:",
|
"manualUpgrade": "Ручное обновление:",
|
||||||
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
|
||||||
"manualUpgradeHint": "Или нажмите \"Обновить сейчас\" для автоматического обновления.",
|
"manualUpgradeHint": "Или нажмите \"Обновить сейчас\" для автоматического обновления.",
|
||||||
"updateCompleted": "Обновление успешно завершено!",
|
"updateCompleted": "Обновление успешно завершено!",
|
||||||
"restartServer": "Пожалуйста, перезапустите сервер для применения изменений.",
|
"restartServer": "Пожалуйста, перезапустите сервер для применения изменений.",
|
||||||
|
|||||||
@@ -105,7 +105,8 @@
|
|||||||
"git": "Git",
|
"git": "Git",
|
||||||
"apiTokens": "API и токены",
|
"apiTokens": "API и токены",
|
||||||
"tasks": "Задачи",
|
"tasks": "Задачи",
|
||||||
"plugins": "Плагины"
|
"plugins": "Плагины",
|
||||||
|
"about": "О программе"
|
||||||
},
|
},
|
||||||
"appearanceSettings": {
|
"appearanceSettings": {
|
||||||
"darkMode": {
|
"darkMode": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "Запустите Claude CLI в каталоге проекта для начала работы"
|
"runClaudeCli": "Запустите Claude CLI в каталоге проекта для начала работы"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code UI",
|
"title": "CloudCLI",
|
||||||
"subtitle": "Интерфейс AI помощника для программирования"
|
"subtitle": "Интерфейс AI помощника для программирования"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,7 +65,12 @@
|
|||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"rename": "Переименовать",
|
"rename": "Переименовать",
|
||||||
"joinCommunity": "Присоединиться к сообществу"
|
"joinCommunity": "Присоединиться к сообществу",
|
||||||
|
"reportIssue": "Сообщить о проблеме",
|
||||||
|
"starOnGithub": "Звезда на GitHub"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"openSource": "Открытый исходный код"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Активен",
|
"active": "Активен",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "欢迎回来",
|
"title": "欢迎回来",
|
||||||
"description": "登录您的 Claude Code UI 账户",
|
"description": "登录您的 CloudCLI 账户",
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"submit": "登录",
|
"submit": "登录",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user