refactor(web): split server.go; consume jcode-ui from npm - #131
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe 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. ChangesWeb API surface
Published UI package consumption
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
internal/web/files.go (2)
128-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
timeconstants over raw nanoseconds.
30*1e9is correct (30s) but obscure;30*time.Secondreads 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 winSize limit is enforced after the full read.
os.ReadFileloads the entire file into memory before the>1MBcheck at Line 102, so a multi-GB file still gets fully read (and could OOM) before being rejected. Stat first and reject oninfo.Size(), or read through anio.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 winAdd
minimumReleaseAgeExcludeupdates 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 theminimumReleaseAgeExcludeentries across allpnpm-workspace.yamlfiles (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
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsite/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (28)
AGENTS.mdexamples/jcode-ui-minimal/package.jsonexamples/jcode-ui-minimal/pnpm-workspace.yamlexamples/jcode-ui-zustand/package.jsonexamples/jcode-ui-zustand/pnpm-workspace.yamlinternal-doc/dynamic-workflow-design.mdinternal/web/approval.gointernal/web/chat.gointernal/web/files.gointernal/web/mcp.gointernal/web/models.gointernal/web/project.gointernal/web/providers.gointernal/web/pty.gointernal/web/remote.gointernal/web/server.gointernal/web/sessions.gointernal/web/setup.gointernal/web/skills.gointernal/web/ws.gopackages/jcode-ui-core/package.jsonpackages/jcode-ui/package.jsonpnpm-workspace.yamlsite/docs/desktop.mdsite/package.jsonsite/pnpm-workspace.yamlweb/package.jsonweb/src/i18n/locales/en.ts
| 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"}) | ||
| } |
There was a problem hiding this comment.
🩺 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 goRepository: 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 goRepository: 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
doneRepository: 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 goRepository: 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 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:
- 1: https://github.com/gorilla/websocket/blob/main/examples/chat/client.go
- 2: Appropriate usage of read and write deadlines gorilla/websocket#473
- 3: https://stackoverflow.com/questions/55201222/how-to-specify-the-frequency-of-ws-pings-with-gorilla
- 4: Websocket ping pong timeout gorilla/websocket#866
- 5: https://github.com/gorilla/websocket/blob/462d5c5828f6203e4047c47be2920315937be380/examples/chat/client.go
- 6: https://github.com/gorilla/websocket/blob/main/doc.go
- 7: SetReadDeadline - Chat example (remove idle clients) gorilla/websocket#960
- 8: https://pkg.go.dev/github.com/gorilla/websocket
🏁 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.goRepository: 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 . || trueRepository: 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' . || trueRepository: cnjack/jcode
Length of output: 5533
Add read deadlines and pong keepalive to the WebSocket loop. internal/web/ws.go:198-229 — conn.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.
…ui-registry refactor(web): split server.go; consume jcode-ui from npm
Summary
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).jcode-ui/jcode-ui-corefrom the npm registry inweb/,site/, and examples — no morefile:/workspace:*links.packages/jcode-uidepends on core via^0.1.0(matches publishedjcode-ui@0.1.1;0.1.0is deprecated for a brokenfile:core dep).AGENTS.md.composables/path references from the Vue → React migration.Test plan
go test ./internal/web/ ./internal/command/pnpm --dir web typechecknpm install jcode-ui@0.1.1importsjcode-ui+ transitivejcode-ui-coreSummary by CodeRabbit