Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,11 @@ script/ # Build-time code generation
- **Output:** builds to `internal/web/dist/`, embedded in Go binary via `//go:embed`
- **Lint:** `cd web && pnpm lint` (eslint + oxlint)
- Changes to the frontend require rebuilding via `make build-web` for the Go binary to pick them up

### Icons & Styling

- **Icons:** use `@heroicons/vue/24/outline` exclusively. Import each icon by name from its subpath (`import { XMarkIcon } from '@heroicons/vue/24/outline'`) for per-file tree-shaking. Do **not** hand-write inline `<svg>` icons or `v-html` SVG path strings.
- **Icon sizing:** use Tailwind `w-N h-N` classes (e.g. `class="w-3.5 h-3.5"`), never a `:size` prop.
- **Colors:** every color must come from a CSS custom property defined in `src/styles/tokens.css`. Never hardcode hex/rgb/`#fff`/`white` in `.vue` or `.css`. Text on the primary/destructive fills uses `--color-on-primary` / `--color-on-destructive`; code blocks use `--code-bg` / `--code-border`; syntax highlighting uses `--hljs-*`.
- **Terminal (xterm) colors:** live in tokens (`--term-*` and the 16-color ANSI palette) and are read at runtime via `getComputedStyle` in `TerminalInstance.vue` — see `termTheme()`. Do not define terminal colors inline.
- **Adding a new color:** add the token to `tokens.css` (both `:root` light and `.dark`) first, then reference it by `var(...)`. To theme it per generated theme, edit `internal/theme/palette.go` and regenerate — never edit `tokens.generated.css` by hand.
Binary file modified desktop/src-tauri/icons/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/128x128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/64x64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square107x107Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square142x142Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square150x150Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square284x284Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square30x30Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square310x310Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square44x44Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square71x71Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/Square89x89Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/StoreLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Binary file modified desktop/src-tauri/icons/icon.icns
Binary file not shown.
Binary file modified desktop/src-tauri/icons/icon.ico
Binary file not shown.
Binary file modified desktop/src-tauri/icons/icon.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-512@2x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png
Binary file modified desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
43 changes: 43 additions & 0 deletions internal/web/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package web

import (
"encoding/json"
"io"
"net/http"
"time"

Expand Down Expand Up @@ -104,5 +105,47 @@ func (s *Server) handleChannelDisable(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "state": "disabled"})
}

// handleChannelBLEStatus reports whether the Bluetooth (BLE) status channel is
// enabled in config. The actual BLE notifier is only wired at startup (and only
// on desktop builds with CoreBluetooth), so this reflects the persisted
// preference, which takes effect on the next launch.
func (s *Server) handleChannelBLEStatus(w http.ResponseWriter, r *http.Request) {
enabled := false
if s.cfg != nil && s.cfg.Channel != nil {
enabled = s.cfg.Channel.BLEEnabled
}
writeJSON(w, http.StatusOK, map[string]any{"enabled": enabled})
Comment on lines +112 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use the same mutex for BLE status reads.

handleSetChannelBLE writes s.cfg.Channel under s.mu, but this GET path reads the same pointer/field without synchronization, so concurrent requests can race.

Proposed fix
 func (s *Server) handleChannelBLEStatus(w http.ResponseWriter, r *http.Request) {
+	s.mu.Lock()
 	enabled := false
 	if s.cfg != nil && s.cfg.Channel != nil {
 		enabled = s.cfg.Channel.BLEEnabled
 	}
+	s.mu.Unlock()
 	writeJSON(w, http.StatusOK, map[string]any{"enabled": enabled})
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *Server) handleChannelBLEStatus(w http.ResponseWriter, r *http.Request) {
enabled := false
if s.cfg != nil && s.cfg.Channel != nil {
enabled = s.cfg.Channel.BLEEnabled
}
writeJSON(w, http.StatusOK, map[string]any{"enabled": enabled})
func (s *Server) handleChannelBLEStatus(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
enabled := false
if s.cfg != nil && s.cfg.Channel != nil {
enabled = s.cfg.Channel.BLEEnabled
}
s.mu.Unlock()
writeJSON(w, http.StatusOK, map[string]any{"enabled": enabled})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/web/channel.go` around lines 112 - 117, The handleChannelBLEStatus
function reads from s.cfg.Channel without synchronization while
handleSetChannelBLE writes to it under the s.mu mutex, creating a race
condition. Wrap the read access to s.cfg.Channel inside a lock acquired from
s.mu (similar to how handleSetChannelBLE protects its write), ensuring the
entire block that accesses s.cfg.Channel and s.cfg is protected by the same
mutex to prevent concurrent read-write races.

}

// handleSetChannelBLE persists the Bluetooth (BLE) status-channel preference.
// Like the proxy/cert settings, it takes effect after an app restart (the BLE
// notifier is created once at startup when channel.ble_enabled is true).
func (s *Server) handleSetChannelBLE(w http.ResponseWriter, r *http.Request) {
var req struct {
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
s.mu.Lock()
if s.cfg == nil {
s.mu.Unlock()
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "config unavailable"})
return
}
if s.cfg.Channel == nil {
s.cfg.Channel = &config.ChannelConfig{}
}
s.cfg.Channel.BLEEnabled = req.Enabled
if err := config.SaveConfig(s.cfg); err != nil {
s.mu.Unlock()
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
Comment on lines +141 to +144

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not return raw config save errors to the client.

SaveConfig wraps errors with the config file path, so err.Error() can disclose local path details. Log the diagnostic and return a stable API error.

Proposed fix
 	if err := config.SaveConfig(s.cfg); err != nil {
 		s.mu.Unlock()
-		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		config.Logger().Printf("[web] BLE channel save config failed: %v", err)
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save BLE setting"})
 		return
 	}

As per coding guidelines, “All diagnostics must go to config.Logger() which writes to ~/.jcode/debug.log.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := config.SaveConfig(s.cfg); err != nil {
s.mu.Unlock()
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
if err := config.SaveConfig(s.cfg); err != nil {
s.mu.Unlock()
config.Logger().Printf("[web] BLE channel save config failed: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save BLE setting"})
return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/web/channel.go` around lines 141 - 144, The error handling in the
SaveConfig call is exposing the raw error to the client via writeJSON, which can
leak sensitive file path information. Instead, log the actual error using
config.Logger() for diagnostic purposes and return a generic, stable error
message to the client. Modify the error handling block after the SaveConfig call
to capture the error, log it with config.Logger(), and then call writeJSON with
a user-safe error message that does not expose internal path details.

Source: Coding guidelines

}
s.mu.Unlock()
writeJSON(w, http.StatusOK, map[string]any{"enabled": req.Enabled})
}

// Ensure writeJSON is used (defined in server.go).
var _ = json.Marshal
11 changes: 11 additions & 0 deletions internal/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ func (s *Server) Start(ctx context.Context) error {
mux.HandleFunc("POST /api/channel/logout", s.handleChannelLogout)
mux.HandleFunc("POST /api/channel/enable", s.handleChannelEnable)
mux.HandleFunc("POST /api/channel/disable", s.handleChannelDisable)
mux.HandleFunc("GET /api/channel/ble", s.handleChannelBLEStatus)
mux.HandleFunc("POST /api/channel/ble", s.handleSetChannelBLE)

// Setup API — available in setup mode (no provider configured yet).
mux.HandleFunc("GET /api/setup/providers", s.handleSetupProviders)
Expand Down Expand Up @@ -2230,6 +2232,15 @@ func (s *Server) handleSetApprovalMode(w http.ResponseWriter, r *http.Request) {
config.Logger().Printf("[web] approval mode agent rebuild error: %v", err)
}
}
// Persist as the default startup mode so the preference survives restarts —
// resolveStartupMode reads cfg.DefaultMode. This makes the Settings toggle a
// true "default", not just a one-off runtime flip.
if s.cfg != nil {
s.cfg.DefaultMode = sm.String()
if err := config.SaveConfig(s.cfg); err != nil {
config.Logger().Printf("[web] approval mode save config failed: %v", err)
}
}
s.mu.Unlock()

s.wsBroker.Broadcast(WSEvent{
Expand Down
2 changes: 1 addition & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/icon.svg">
<link rel="icon" type="image/png" href="/icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#111111">
<script>
Expand Down
3 changes: 1 addition & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@headlessui/tailwindcss": "^0.2.2",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2",
"@tauri-apps/api": "^2.9.0",
Expand All @@ -30,8 +31,6 @@
"@xterm/xterm": "^6.0.0",
"dompurify": "^3.4.11",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^1.0.0",
"marked": "^18.0.0",
"marked": "^18.0.2",
"marked-highlight": "^2.2.4",
"pinia": "^3.0.4",
Expand Down
25 changes: 12 additions & 13 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added web/public/icon.png
55 changes: 0 additions & 55 deletions web/public/icon.svg
Diff not rendered.
45 changes: 23 additions & 22 deletions web/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, nextTick, watch, onUnmounted, provide } from 'vue'
import { ChevronDoubleDownIcon } from '@heroicons/vue/24/outline'
import { normalizeMode } from '@/types/api'
import type { RemoteMeta } from '@/types/api'
import { useChatStore } from '@/stores/chat'
Expand Down Expand Up @@ -434,7 +435,7 @@ function startResize(e: MouseEvent) {
Start a new task in <span class="welcome-project">{{ store.projectName || 'jcode' }}</span>
</h2>
<p class="welcome-sub">
Pick a workspace, then send a message. Use <kbd class="welcome-kbd">/</kbd> for commands.
Send a message to start. <kbd class="welcome-kbd">/</kbd> for commands.
</p>
</div>

Expand All @@ -460,10 +461,8 @@ function startResize(e: MouseEvent) {
<ChatMessageVue
v-if="item.kind === 'message'"
:message="item.data"
:can-retry="item.data.role === 'assistant' && !store.isRunning"
:can-edit="item.data.role === 'user' && !store.isRunning"
class="animate-fade-up"
@retry="store.retryFromMessage(item.data.id)"
@edit="(text) => store.editAndResend(item.data.id, text)"
/>
<ToolCallCard v-else-if="item.kind === 'tool'" :tool="item.data" class="animate-fade-up pl-9" />
Expand Down Expand Up @@ -506,25 +505,27 @@ function startResize(e: MouseEvent) {
</div>

<!-- Scroll-to-bottom button -->
<transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-2"
>
<button
v-if="showScrollBtn"
class="absolute bottom-40 left-1/2 -translate-x-1/2 z-10 w-8 h-8 flex items-center justify-center rounded-full shadow-lg cursor-pointer transition-colors"
style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-muted-foreground)"
@click="scrollToBottom()"
<!-- Scroll-to-bottom button — anchored as its own flex row above the
composer (no magic px offset), so it tracks composer height. -->
<div class="relative h-0">
<transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-2"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M12 5v14M5 12l7 7 7-7" />
</svg>
</button>
</transition>
<button
v-if="showScrollBtn"
class="absolute bottom-2 left-1/2 -translate-x-1/2 z-10 w-8 h-8 flex items-center justify-center rounded-full shadow-lg cursor-pointer transition-colors"
style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-muted-foreground)"
@click="scrollToBottom()"
>
<ChevronDoubleDownIcon class="w-4 h-4" />
</button>
</transition>
</div>

<GoalBanner />
<ChatInput />
Expand Down Expand Up @@ -639,7 +640,7 @@ function startResize(e: MouseEvent) {
border: none;
border-radius: var(--radius-lg);
background: var(--color-primary);
color: #fff;
color: var(--color-on-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
Expand Down
Loading
Loading