Skip to content

Add Server section to the Inspector sidebar - #155

Open
NRohner wants to merge 14 commits into
drumih:mainfrom
NRohner:feat/server-ui-controls
Open

Add Server section to the Inspector sidebar#155
NRohner wants to merge 14 commits into
drumih:mainfrom
NRohner:feat/server-ui-controls

Conversation

@NRohner

@NRohner NRohner commented Aug 25, 2026

Copy link
Copy Markdown

Summary

  • Adds a "Server" section to the Mac app's Inspector sidebar with Start/Stop controls for TurboFieldfareServer, the local OpenAI-compatible inference server — previously only startable from a terminal.
  • The server is spawned as a plain child process, configured entirely from the app's existing model/runtime settings (modelPathText, maxContextTokens, runtimeOptions) plus two new settings, port (default 8080) and queue limit (default 4). --vision-pack is added automatically when a vision pack is installed.
  • Starting the server does not require unloading the app's own model; the UI shows a warning caption instead of blocking, per an explicit design decision.
  • The server is stopped gracefully (SIGTERM) when the app quits.
  • MacAppSettings schema bumped 2→3 for the two new persisted fields, backward compatible with older settings files.

Full design spec: docs/superpowers/specs/2026-08-25-server-ui-controls-design.md
Full implementation plan: docs/superpowers/plans/2026-08-25-server-ui-controls.md

Built via subagent-driven development: 7 tasks, each independently reviewed for spec compliance and code quality, plus a final whole-branch review that caught and fixed an unbounded-memory issue in stderr capture, an untested code path, and a UX gap around the server's buffered-signal shutdown window.

Test plan

  • Full suite passing: 1033/1033 (Scripts/test.sh)
  • swift build -c debug (both the core library and the TurboFieldfareMac app target)
  • Manually verified in the running app: Server section renders correctly in the Inspector sidebar (State/Port/Queue limit/Start button)
  • Full Start → Running → Stop → Stopped cycle against a real installed model (state machine covered by unit tests; not exercised interactively in this environment)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ

NRohner and others added 14 commits August 25, 2026 08:55
Covers spawning TurboFieldfareServer as a sibling process using the
app's existing model/runtime settings, start/stop state handling, and
new port/queue-limit settings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
Task-by-task TDD plan covering AppServerState/AppServerController,
the argument builder, MacAppSettings v3, ProcessServerController,
AppModel wiring, the Inspector UI, and app-quit cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
Wires AppServerController/AppServerState/AppServerArguments (Tasks 1-2)
and the persisted serverPort/serverQueueLimit settings (Task 3) into
AppModel: a serverController dependency, serverState/serverPort/
serverQueueLimit public state, canStartServer/canStopServer/
canEditServerSettings gating, and startServer/stopServer/
stopServerForTermination/setServerPort/setServerQueueLimit control
methods. applyServerState mirrors the existing applyLoadState pattern,
guarding a generation counter against a stale async callback from a
start the model has since moved past.

Also fixes a strict-concurrency compile error in the new test file's
waitUntil helper (missing @mainactor on the function itself, matching
the existing pattern in AppModelInstallTests.swift) that the plan's
literal code did not have.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
Task 3 raised MacAppSettings.currentVersion to 3, which collided with
this test's hardcoded version-3 fixture meant to simulate a schema
newer than whatever this build currently understands. Derive the
fixture version from currentVersion + 1 instead of a literal, so the
test keeps testing "an unknown future version" regardless of where
currentVersion moves next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
… state

AppModel computes installationStatus once in init from whichever
directory is passed there (defaulting to AppModelLocation.defaultURL()
when omitted). Reassigning modelPathText afterward doesn't recompute
it, so the test passed only on a machine with no model installed at
the default location. Construct AppModel with an explicit, guaranteed-
missing directory instead, so the test is hermetic regardless of local
scratch state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
…uards

ProcessServerController reports .running from a stdout readability
handler and .stopped/.failed from Process.terminationHandler, two
independent unsynchronized callback paths. The prior generation-only
guard let a late .running overwrite an already-applied terminal
outcome depending on which callback's MainActor task ran last. Add a
serverReachedTerminalState latch: once .stopped/.failed lands for the
current generation, nothing else can change serverState until the
next startServer() call.

Also guard setServerPort/setServerQueueLimit against out-of-range
values, matching MacAppSettings.isValid()'s bounds -- an invalid value
previously got accepted into memory while persistSettings() silently
no-op'd on every subsequent save, including unrelated settings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
State/Port/Queue limit rows and a Start/Stop button, wired to the
AppModel server-control state machine added in prior tasks. Port and
queue-limit fields bind through setServerPort/setServerQueueLimit
(not directly to the raw properties) so their range validation stays
in effect from the UI layer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
Bounds the stderr buffer ProcessServerController accumulates for the life
of a server process and truncates the failure message it surfaces, since
routine per-request logging on stderr could otherwise grow unbounded and
get rendered whole in the Inspector sidebar on a crash. Also clears
self.process in the termination handler so a crash right as Stop is
clicked can't retain the old Pipes/buffer or terminate a reaped process.

Adds direct unit tests for port(fromReadyOutput:), the only untested logic
path that detects a successful server start, by exposing it as internal
rather than private so the test target can reach it.

Makes the "Stopping..." state explain itself when the stop was requested
during .starting: SIGTERM is buffered by the server until the model finishes
loading, so the UI could otherwise sit disabled for minutes with no visible
reason why.

Also: shows the double-memory-load caption during .starting (not just when
canStartServer, which is false exactly then), stops a vision-pack download
from blocking the unrelated Stop Server button, tightens serverPort/
serverQueueLimit to private(set), and gives three tests an explicit missing
model directory instead of falling back to the real default location.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
stopServer() only ever set the flag to true, never cleared it, so a
stop requested during .starting left it stuck true even after the
buffered SIGTERM let the server finish loading and briefly report
.running (the terminal-state latch doesn't cover .running, by
design). A second Stop click after that flash then showed "waiting
for the model to finish loading" for a server whose model had already
loaded. Make the assignment unconditional instead of set-only-true.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7gXjo83uLkvj1HrDNEMQ
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