Skip to content

refactor(web): split server.go; consume jcode-ui from npm - #131

Merged
cnjack merged 3 commits into
mainfrom
chore/web-server-split-and-jcode-ui-registry
Jul 10, 2026
Merged

refactor(web): split server.go; consume jcode-ui from npm#131
cnjack merged 3 commits into
mainfrom
chore/web-server-split-and-jcode-ui-registry

Conversation

@cnjack

@cnjack cnjack commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Split internal/web/server.go (~4k lines) into domain files (chat, sessions, models, mcp, providers, setup, …) and move PTY/WS/SSH handlers into existing modules. Behavior is unchanged (same package, mechanical extract).
  • Consume jcode-ui / jcode-ui-core from the npm registry in web/, site/, and examples — no more file: / workspace:* links. packages/jcode-ui depends on core via ^0.1.0 (matches published jcode-ui@0.1.1; 0.1.0 is deprecated for a broken file: core dep).
  • Document the publish order, checklist, and registry-consumer contract in AGENTS.md.
  • Clean up leftover composables/ path references from the Vue → React migration.

Test plan

  • go test ./internal/web/ ./internal/command/
  • pnpm --dir web typecheck
  • Clean-dir npm install jcode-ui@0.1.1 imports jcode-ui + transitive jcode-ui-core
  • CI green on this PR

Summary by CodeRabbit

  • New Features
    • Expanded web UI capabilities for chat, run stopping, sessions/tasks, projects, approvals, models, providers, skills, MCP servers, files, PTY/terminal sessions, and SSH environments.
    • Added OAuth-based MCP server login with status/progress and browser-opening flow.
    • Enabled real-time WebSocket updates for changes across tasks, mode, models, approvals, MCP, and projects.
  • Documentation
    • Updated publishing and frontend/desktop guidance, including how to consume UI packages from the npm registry.
  • Chores
    • Bumped UI package versions and updated examples/site/web to depend on published releases.

t added 2 commits July 10, 2026 15:22
Move API handlers out of the 4k-line server.go into focused package
files (chat, sessions, models, mcp, providers, …) and append PTY/WS/SSH
handlers to existing modules. Also fix leftover composables path
references after the Vue → React migration.
Point web/site/examples and jcode-ui→core at registry version ranges
instead of file:/workspace links so local installs match published
tarballs. Bump jcode-ui to 0.1.1 (0.1.0 had a broken file: core dep).
Document the publish flow and registry-consumer contract in AGENTS.md.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e73c1f2-e82e-440b-8ed4-07047640a40f

📥 Commits

Reviewing files that changed from the base of the PR and between f3092d6 and 36b7a74.

📒 Files selected for processing (4)
  • AGENTS.md
  • internal/web/files.go
  • internal/web/models.go
  • internal/web/providers.go
✅ Files skipped from review due to trivial changes (1)
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/web/providers.go
  • internal/web/models.go
  • internal/web/files.go

📝 Walkthrough

Walkthrough

The PR adds web API handlers for agent sessions, workspace operations, providers, models, MCP servers, skills, PTYs, and WebSockets. It also changes UI consumers to published npm package versions and updates package publishing and frontend source-path documentation.

Changes

Web API surface

Layer / File(s) Summary
Agent execution and session lifecycle
internal/web/approval.go, internal/web/chat.go, internal/web/sessions.go
Adds chat execution, stop, goal, approval, ask-user, task metadata, session loading, deletion, truncation, and resume handlers.
Workspace, project, PTY, and diff operations
internal/web/files.go, internal/web/project.go, internal/web/pty.go, internal/web/remote.go
Adds workspace-restricted file access, command execution, git diff modes, project switching, PTY management, and SSH alias listing.
Provider, model, and setup management
internal/web/providers.go, internal/web/models.go, internal/web/setup.go
Adds provider catalogs and CRUD, setup validation, model listing and switching, mode changes, persisted model preferences, and reasoning-effort overrides.
MCP and skill management
internal/web/mcp.go, internal/web/skills.go
Adds MCP CRUD, asynchronous reloads, OAuth login state, skill toggling, and slash-command listing.
WebSocket connection and message routing
internal/web/ws.go
Adds WebSocket upgrades, client registration, subscriptions, ping handling, and task-scoped approval messages.

Published UI package consumption

Layer / File(s) Summary
Published package metadata and consumer dependencies
packages/jcode-ui*/package.json, web/package.json, site/package.json, examples/*/package.json, pnpm-workspace.yaml, site/pnpm-workspace.yaml, examples/*/pnpm-workspace.yaml
Replaces local UI package links with published semver dependencies, updates package metadata, and excludes published versions from pnpm release-age checks.
Publishing and source-path documentation
AGENTS.md, internal-doc/dynamic-workflow-design.md, site/docs/desktop.md, web/src/i18n/locales/en.ts
Updates publishing guidance and corrects documented frontend source paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant WebServer
  participant Engine
  participant ConfigStore
  participant WebSocketBroker
  Browser->>WebServer: HTTP request
  WebServer->>ConfigStore: load or save configuration
  WebServer->>Engine: switch, rebuild, or run task
  Engine-->>WebServer: result or state update
  WebServer->>WebSocketBroker: broadcast state event
  WebSocketBroker-->>Browser: UI update
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the two main changes: splitting server.go into domain files and switching jcode-ui to npm packages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/web-server-split-and-jcode-ui-registry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
internal/web/files.go (2)

128-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer time constants over raw nanoseconds.

30*1e9 is correct (30s) but obscure; 30*time.Second reads clearly.

🤖 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/files.go` at line 128, Replace the raw nanosecond timeout in the
context creation with the standard library’s time constant, using 30*time.Second
in the context.WithTimeout call and ensuring the time package is imported.

95-107: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Size limit is enforced after the full read.

os.ReadFile loads the entire file into memory before the >1MB check at Line 102, so a multi-GB file still gets fully read (and could OOM) before being rejected. Stat first and reject on info.Size(), or read through an io.LimitReader.

♻️ Proposed approach
+	if info, err := os.Stat(abs); err == nil && info.Size() > 1<<20 {
+		writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "file too large (>1MB)"})
+		return
+	}
 	content, err := os.ReadFile(abs)
🤖 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/files.go` around lines 95 - 107, In the file-serving handler,
the current os.ReadFile call loads the entire file before enforcing the 1MB
limit. Update the handler to check os.Stat result.Size() before reading and
return the existing 413 response for oversized files, or use a bounded
io.LimitReader; preserve the existing not-found and successful response
behavior.
AGENTS.md (1)

70-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add minimumReleaseAgeExclude updates to the publishing checklist.

The post-publish guidance (line 95) and checklist (lines 97–101) mention bumping consumer deps and running pnpm install, but don't mention updating the minimumReleaseAgeExclude entries across all pnpm-workspace.yaml files (root, site/, examples/*). When a new version is published, the old exclude entry becomes stale and the new version may trigger release-age enforcement failures in CI.

📝 Suggested addition to the checklist
 Checklist before publish:
 
 1. `jcode-ui` → `jcode-ui-core` is a registry range (`^x.y.z`), not `file:`
 2. Both packages have fresh `dist/` (`pnpm build`)
 3. Smoke: `npm install jcode-ui@<ver>` in a temp dir imports both packages and pulls core transitively
+4. After publish: update `minimumReleaseAgeExclude` entries in all `pnpm-workspace.yaml` files (root, `site/`, `examples/*`) to the newly published versions
🤖 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 `@AGENTS.md` around lines 70 - 101, Add updating minimumReleaseAgeExclude
entries in every pnpm-workspace.yaml file (root, site/, and examples/*) to the
post-publish guidance and checklist. Instruct that stale package versions must
be replaced with the newly published jcode-ui and jcode-ui-core versions before
running installs or CI checks.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@internal/web/files.go`:
- Around line 196-208: Remove the unused statCmd construction and execution
block from the relevant file-status handler, including its mode-specific command
setup and CombinedOutput call. Keep the existing countDiffLines-based
additions/deletions logic unchanged, since the discarded --stat output is not
used in the response.

In `@internal/web/models.go`:
- Around line 19-55: In handleListModels, snapshot s.cfg and s.registry under
s.cfgMu before any nil checks or subsequent use, then use those local snapshots
throughout the handler, including model registry construction and provider
access. Update handleSetupComplete and handleUpdateProvider assumptions as
needed so all reads of these reassigned pointers are synchronized.

In `@internal/web/providers.go`:
- Around line 414-419: After the successful config.SaveConfig call in
handleAddProvider, update the live s.cfg provider configuration and registry
using the same provider data that was persisted, before returning the success
response. Ensure subsequent /api/models requests immediately include the newly
added provider without requiring a restart.
- Around line 491-635: Serialize the entire provider config read-modify-write
flow in the provider update handler under the same mutex used by other config
saves, including LoadConfig, mutation, SaveConfig, and live state publication.
Use the shared config mutex consistently with server.go and reconcile mcp.go’s
s.mu usage so all config mutations and disk writes use one common lock,
preventing concurrent updates from overwriting each other.
- Around line 657-676: When deleting the active provider in the
provider-deletion handler, update cfg.Model to reference a surviving provider
and valid model before saving, or reject deletion when no safe replacement
exists. After the deletion succeeds, republish the live s.cfg and s.registry
state so the running server immediately reflects the change; use the existing
provider deletion handler and config publication mechanisms to locate the
changes.

In `@internal/web/ws.go`:
- Around line 218-229: Update the WebSocket connection setup and read loop
around handleWSMessage to prevent half-open connections: define a pong wait
interval, set an initial read deadline, install a PongHandler that refreshes the
deadline, and run a write-side ticker that sends periodic Ping control frames;
ensure the ticker and writer stop on disconnect and terminate the client when
ping or write errors occur.

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 70-101: Add updating minimumReleaseAgeExclude entries in every
pnpm-workspace.yaml file (root, site/, and examples/*) to the post-publish
guidance and checklist. Instruct that stale package versions must be replaced
with the newly published jcode-ui and jcode-ui-core versions before running
installs or CI checks.

In `@internal/web/files.go`:
- Line 128: Replace the raw nanosecond timeout in the context creation with the
standard library’s time constant, using 30*time.Second in the
context.WithTimeout call and ensuring the time package is imported.
- Around line 95-107: In the file-serving handler, the current os.ReadFile call
loads the entire file before enforcing the 1MB limit. Update the handler to
check os.Stat result.Size() before reading and return the existing 413 response
for oversized files, or use a bounded io.LimitReader; preserve the existing
not-found and successful response behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f6e6fec-168a-4b2a-8d0f-e13a3112e8ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3daff2a and f3092d6.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • site/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • AGENTS.md
  • examples/jcode-ui-minimal/package.json
  • examples/jcode-ui-minimal/pnpm-workspace.yaml
  • examples/jcode-ui-zustand/package.json
  • examples/jcode-ui-zustand/pnpm-workspace.yaml
  • internal-doc/dynamic-workflow-design.md
  • internal/web/approval.go
  • internal/web/chat.go
  • internal/web/files.go
  • internal/web/mcp.go
  • internal/web/models.go
  • internal/web/project.go
  • internal/web/providers.go
  • internal/web/pty.go
  • internal/web/remote.go
  • internal/web/server.go
  • internal/web/sessions.go
  • internal/web/setup.go
  • internal/web/skills.go
  • internal/web/ws.go
  • packages/jcode-ui-core/package.json
  • packages/jcode-ui/package.json
  • pnpm-workspace.yaml
  • site/docs/desktop.md
  • site/package.json
  • site/pnpm-workspace.yaml
  • web/package.json
  • web/src/i18n/locales/en.ts

Comment thread internal/web/files.go Outdated
Comment thread internal/web/models.go Outdated
Comment thread internal/web/providers.go
Comment thread internal/web/providers.go
Comment on lines +491 to +635
cfg, err := config.LoadConfig()
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
pc := cfg.GetProviders()[id]
if pc == nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "provider not found"})
return
}

// Mutate in place so fields not exposed by this endpoint (display name,
// custom models, deprecated lists) are preserved untouched.
prevHeaders := pc.Headers
// base_url uses keep-on-empty semantics (like api_key): the list endpoint
// masks secrets but returns base_url verbatim, yet a client that doesn't
// touch the endpoint may still submit an empty value. Overwriting
// unconditionally would wipe a stored custom endpoint, so only adopt a
// non-empty incoming value.
if req.BaseURL != "" {
pc.BaseURL = req.BaseURL
}
pc.Vision = req.Vision
pc.Thinking = req.Thinking
pc.ReasoningEffort = req.ReasoningEffort
if req.Name != "" {
pc.Name = req.Name
}
if req.APIKey != "" {
pc.APIKey = req.APIKey
}
// Merge headers: empty incoming value ⇒ keep the stored secret for that key.
pc.Headers = nil
if cleaned := cleanHeaders(req.Headers); len(cleaned) > 0 {
merged := make(map[string]string, len(cleaned))
for k, v := range cleaned {
if v == "" {
if ov, ok := prevHeaders[k]; ok {
merged[k] = ov
continue
}
}
merged[k] = v
}
pc.Headers = merged
}

// Replace the provider's custom models when the client sends the list (nil ⇒
// keep existing). Each model's stored Context is preserved by merging on id,
// ToolCall stays true (matching the add path), and the model currently set as
// active cannot be dropped so a save can't strand the running app.
if req.CustomModels != nil {
prev := make(map[string]config.CustomModelConfig, len(pc.CustomModels))
for _, m := range pc.CustomModels {
prev[m.ID] = m
}
next := make([]config.CustomModelConfig, 0, len(*req.CustomModels))
seen := make(map[string]bool, len(*req.CustomModels))
for _, m := range *req.CustomModels {
mid := strings.TrimSpace(m.ID)
if mid == "" || seen[mid] {
continue
}
seen[mid] = true
cm := config.CustomModelConfig{ID: mid, Name: strings.TrimSpace(m.Name), ToolCall: true, Reasoning: m.Reasoning}
// Adopt the incoming per-model capability fields when provided;
// otherwise carry over the previously stored values so an edit that
// only renames a model doesn't silently drop its context window,
// vision flag, or configured effort tiers.
if old, ok := prev[mid]; ok {
if cm.Context == 0 {
cm.Context = old.Context
}
if !cm.Attachment {
cm.Attachment = old.Attachment
}
if len(cm.EffortTiers) == 0 {
cm.EffortTiers = old.EffortTiers
}
}
if m.Context > 0 {
cm.Context = m.Context
}
if m.Attachment {
cm.Attachment = true
}
if len(m.EffortTiers) > 0 {
cm.EffortTiers = m.EffortTiers
}
next = append(next, cm)
}
// Reject custom model ids that collide with the provider's built-in
// (registry) models. A duplicate id would shadow or be shadowed by the
// registry entry, confusing the model picker and catalog. Custom ids
// may still be edited to their own value (handled by seen dedup above).
if s.registry != nil {
if regProv := s.registry.GetProvider(id); regProv != nil {
for _, cm := range next {
if _, ok := regProv.Models[cm.ID]; ok {
// Allow it only if it was already a custom model with this id
// (editing an existing custom entry in place).
if _, wasCustom := prev[cm.ID]; !wasCustom {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "model id '" + cm.ID + "' duplicates a built-in model; choose another id",
})
return
}
}
}
}
}
if strings.HasPrefix(cfg.Model, id+"/") {
active := strings.TrimPrefix(cfg.Model, id+"/")
if _, wasThere := prev[active]; wasThere && !seen[active] {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot remove the active model; switch to another model first"})
return
}
}
isCustom := s.registry == nil || !s.registry.HasProvider(id)
if isCustom && len(next) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "custom providers need at least one model"})
return
}
pc.CustomModels = next
}

if cfg.Providers == nil {
cfg.Providers = make(map[string]*config.ProviderConfig)
}
cfg.Providers[id] = pc
if err := config.SaveConfig(cfg); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save config: " + err.Error()})
return
}

// Publish the updated config + registry to the live server so the chat model
// picker (/api/models) and catalog reflect added/edited/removed models
// without a restart — matching handleSetupComplete's publish step.
s.cfgMu.Lock()
s.cfg = cfg
s.registry = model.NewModelRegistryWithConfig(cfg)
s.cfgMu.Unlock()

writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all s.cfg mutations/assignments and the lock held around them.
rg -nP -C4 '\bs\.cfg\b' internal/web --type go
echo '--- cfgMu vs mu usage ---'
rg -nP '\bs\.(cfgMu|mu)\.(Lock|RLock)\b' internal/web --type go

Repository: cnjack/jcode

Length of output: 19616


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal/web/providers.go excerpt ---'
sed -n '480,640p' internal/web/providers.go | cat -n

echo '--- server lock definitions / docs ---'
rg -n -C3 'cfgMu|type Server struct|serialize read-modify-write|cfgMu serializes|s\.mu' internal/web/server.go internal/web/*.go --type go

Repository: cnjack/jcode

Length of output: 30101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- config package files ---'
git ls-files config internal | rg '(^config/|^internal/.*/config|config\.go$|save.*config|load.*config)' || true

echo '--- SaveConfig / LoadConfig definitions ---'
rg -n -C4 'func (.*LoadConfig|func (.*SaveConfig|type Config struct|sync\.(Mutex|RWMutex))' config internal --type go

echo '--- config package excerpts ---'
for f in $(git ls-files 'config/*.go'); do
  echo "### $f"
  sed -n '1,260p' "$f" | cat -n
done

Repository: cnjack/jcode

Length of output: 410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal/config/config.go ---'
wc -l internal/config/config.go
sed -n '1,260p' internal/config/config.go | cat -n

echo '--- all cfgMu usage with surrounding context ---'
rg -n -C4 'cfgMu|LoadConfig\(|SaveConfig\(' internal/web --type go

Repository: cnjack/jcode

Length of output: 28809


Serialize provider config saves under s.cfgMu.
This handler does LoadConfig → mutate → SaveConfig without the mutex that internal/web/server.go says serializes config RMW, so concurrent provider edits (and edits racing other config saves) can overwrite each other. mcp.go still mutates/saves the same config under s.mu, so the live config and disk writes are protected by different locks.

🤖 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/providers.go` around lines 491 - 635, Serialize the entire
provider config read-modify-write flow in the provider update handler under the
same mutex used by other config saves, including LoadConfig, mutation,
SaveConfig, and live state publication. Use the shared config mutex consistently
with server.go and reconcile mcp.go’s s.mu usage so all config mutations and
disk writes use one common lock, preventing concurrent updates from overwriting
each other.

Comment thread internal/web/providers.go Outdated
Comment thread internal/web/ws.go
Comment on lines +218 to +229
for {
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
var incoming WSIncoming
if err := json.Unmarshal(msg, &incoming); err != nil {
continue
}
s.handleWSMessage(client, incoming)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

gorilla websocket SetReadDeadline SetPongHandler keepalive recommended pattern

💡 Result:

The recommended pattern for implementing keepalive in the gorilla/websocket package is to use a combination of heartbeats (Ping/Pong frames) and connection deadlines to detect and close idle or broken connections [1][2][3]. Because the library is designed to be low-level, it does not manage keepalives automatically; you must implement the logic in your application [4][3]. The authoritative pattern, as demonstrated in the official chat example, involves two main goroutines per connection: a reader and a writer [1][5]. Reader (readPump) Implementation: 1. Set a read deadline initially [1]. 2. Define a PongHandler that resets the read deadline whenever a Pong message is received [1]. 3. The read loop calls ReadMessage, which processes control frames (including Pongs) automatically [6]. If the deadline expires without receiving data (either a message or a Pong), ReadMessage returns an error, signaling the connection is dead [7][8]. Example snippet for the reader: // Set the initial read deadline c.conn.SetReadDeadline(time.Now.Add(pongWait)) // Set the pong handler to reset the deadline upon receiving a pong c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now.Add(pongWait)) return nil }) // Main read loop for { _, _, err:= c.conn.ReadMessage if err!= nil { // Handle connection closure or error break } // Reset deadline on any received message c.conn.SetReadDeadline(time.Now.Add(pongWait)) } Writer (writePump) Implementation: 1. Use a time.Ticker to send Ping messages at regular intervals [1]. 2. Set a write deadline before every write operation (including Pings) to prevent the goroutine from blocking indefinitely if the peer is unresponsive [1][4][2]. Key Best Practices: - Keepalive Timing: Ensure your ping interval is shorter than your pongWait (the read deadline duration) [1]. A common ratio is pingPeriod = (pongWait * 9) / 10 [1]. - Concurrency: You must ensure that no more than one goroutine calls the read methods (including SetPongHandler) and no more than one goroutine calls the write methods concurrently [6][1]. - Deadlines: Always use SetReadDeadline and SetWriteDeadline to protect against idle or stalled clients [2]. If you do not send Pings, you should clear the read deadline after the initial handshake if you wish to keep the connection open indefinitely, though this risks leaving "zombie" connections on the server [2]. Top results: [1], [2], [3]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby websocket-related symbols.
ast-grep outline internal/web/ws.go --view expanded || true

printf '\n--- file size ---\n'
wc -l internal/web/ws.go

printf '\n--- targeted slice around handleWebSocket ---\n'
sed -n '1,320p' internal/web/ws.go

Repository: cnjack/jcode

Length of output: 8924


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for websocket keepalive/deadline logic across the repo.
rg -n --hidden -S 'SetReadDeadline|SetPongHandler|SetPingHandler|WriteControl|PingMessage|PongMessage|ReadMessage\(|WriteMessage\(' internal . || true

Repository: cnjack/jcode

Length of output: 2540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- internal/browser/bridge.go (deadline/keepalive slice) ---'
sed -n '60,240p' internal/browser/bridge.go

printf '\n%s\n' '--- websocket ping/pong message usage in repo ---'
rg -n --hidden -S '"ping"|\"pong\"|type.*ping|type.*pong|SetPongHandler|SetPingHandler' . || true

Repository: cnjack/jcode

Length of output: 5533


Add read deadlines and pong keepalive to the WebSocket loop. internal/web/ws.go:198-229conn.ReadMessage() can hang forever on a half-open peer, leaving the goroutine and broker entry stuck until process exit. App-level "ping"/"pong" messages don’t keep the transport alive; set a read deadline, add a Pong handler, and send periodic Ping control frames from the write side.

🤖 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/ws.go` around lines 218 - 229, Update the WebSocket connection
setup and read loop around handleWSMessage to prevent half-open connections:
define a pong wait interval, set an initial read deadline, install a PongHandler
that refreshes the deadline, and run a write-side ticker that sends periodic
Ping control frames; ensure the ticker and writer stop on disconnect and
terminate the client when ping or write errors occur.

Publish live cfg/registry after provider add/update/delete under cfgMu,
repoint cfg.Model when deleting the active provider, snapshot cfg in
handleListModels, drop the unused git --stat work, reject oversized
file reads via Stat, and document minimumReleaseAgeExclude in AGENTS.md.
@cnjack
cnjack merged commit f437dfa into main Jul 10, 2026
2 checks passed
@cnjack
cnjack deleted the chore/web-server-split-and-jcode-ui-registry branch July 10, 2026 08:49
cnjack added a commit that referenced this pull request Jul 12, 2026
…ui-registry

refactor(web): split server.go; consume jcode-ui from npm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant