Skip to content

v0.20 PR 6: Settings page restructure (phantom.yaml, 6 sections, 7 bug fixes)#76

Merged
mcheemaa merged 6 commits intomainfrom
v0.20-pr-06-settings-restructure
Apr 17, 2026
Merged

v0.20 PR 6: Settings page restructure (phantom.yaml, 6 sections, 7 bug fixes)#76
mcheemaa merged 6 commits intomainfrom
v0.20-pr-06-settings-restructure

Conversation

@mcheemaa
Copy link
Copy Markdown
Member

Summary

  • Reframes the Settings page from a curated form over ~/.claude/settings.json (Agent SDK internals) to an operator-facing form over config/phantom.yaml. Six sections: Identity, Model + cost, Evolution, Channels, Memory, Permissions.
  • New GET/PUT /ui/api/phantom-config endpoint with Zod-derived UI schema, atomic YAML writes, and per-field audit rows tagged by section.
  • Removes 19 SDK-internal fields that had no operator use case; adds 16 new operator knobs (identity, cost budget, evolution cadence, channel on/off, memory limits, tool permissions).
  • Every channel secret, every API key, and every webhook secret stays .env/channels.yaml only. The Zod schema uses .strict() so attempting to write a secret through PUT returns 400 at parse time.
  • Mid-write failure path is explicitly tested: a thrown rename leaves phantom.yaml byte-identical to before and no audit row is written.

Verdicts on the 30 existing fields

# Field Section Verdict Reason
1 permissions.allow Permissions KEEP Real operator knob; moves from settings.json to phantom.yaml.
2 permissions.deny Permissions KEEP Same.
3 permissions.ask Permissions REMOVE Ask rules blur intent; agents autonomous.
4 permissions.defaultMode Permissions KEEP Primary access policy.
5 permissions.additionalDirectories Permissions REMOVE Agent workspace is container-fixed.
6 permissions.disableBypassPermissionsMode Permissions REMOVE Irreversible, SDK-internal, undermines autonomy.
7 model Model+cost CHANGE Moves from settings.json to phantom.yaml:model with a select UI.
8 effortLevel Model+cost CHANGE Same; moves to phantom.yaml:effort.
9 enabledMcpjsonServers MCP REMOVE Dead in Phantom (uses in-process MCP).
10 disabledMcpjsonServers MCP REMOVE Dead.
11 enableAllProjectMcpServers MCP REMOVE Dead.
12 disableAllHooks Hooks REMOVE Duplicated by Hooks tab.
13 defaultShell Hooks REMOVE Linux-only container.
14 allowedHttpHookUrls Hooks REMOVE Belongs in Hooks tab.
15 httpHookAllowedEnvVars Hooks REMOVE Belongs in Hooks tab.
16 autoMemoryEnabled Memory REMOVE SDK memory parallel to Qdrant.
17 autoDreamEnabled Memory REMOVE Same; evolution reflection replaces it.
18 claudeMdExcludes Memory REMOVE No VM use case.
19 cleanupPeriodDays Session REMOVE Governs SDK transcript store, not Phantom chat_sessions.
20 respectGitignore Session REMOVE SDK default is correct.
21 includeCoAuthoredBy Session REMOVE Global user pref; layering violation.
22 includeGitInstructions Session REMOVE Prompt assembler owns it.
23 alwaysThinkingEnabled UI REMOVE SDK-internal.
24 showThinkingSummaries UI REMOVE SDK-internal.
25 fastMode UI REMOVE SDK-internal.
26 prefersReducedMotion UI REMOVE SDK-internal.
27 outputStyle UI REMOVE SDK-internal.
28 language UI REMOVE SDK-internal.
29 autoUpdatesChannel Updates REMOVE CLI is container-pinned.
30 minimumVersion Updates REMOVE Same.

KEEP: 3 ▪ CHANGE: 2 ▪ REMOVE: 25. Only 5 of the 30 fields survive, and 2 of those 5 migrate from settings.json to phantom.yaml.

New operator controls

Section Field Control Backing store
Identity Agent name text phantom.yaml:name
Identity Role select phantom.yaml:role
Identity Public URL text (URL) phantom.yaml:public_url
Identity Subdomain root text phantom.yaml:domain
Model+cost Model select with custom fallback phantom.yaml:model
Model+cost Effort select low/medium/high/max phantom.yaml:effort
Model+cost Judge model select with "same as main" phantom.yaml:judge_model
Model+cost Max budget USD number phantom.yaml:max_budget_usd
Model+cost Timeout (minutes) number phantom.yaml:timeout_minutes
Evolution Reflection select auto/always/never phantom.yaml:evolution.reflection_enabled
Evolution Cadence minutes number + preset chips phantom-config/meta/evolution.json + phantom.yaml mirror
Evolution Demand trigger depth number Same
Channels Slack/Telegram/Email/Webhook enabled 4 toggles channels.yaml:*.enabled
Memory Qdrant URL text (URL) config/memory.yaml:qdrant.url
Memory Ollama URL text (URL) config/memory.yaml:ollama.url
Memory Embedding model text config/memory.yaml:ollama.model
Memory Episodes/facts/procedures per query 3 numbers config/memory.yaml:context.*
Permissions Default mode select phantom.yaml:permissions.default_mode
Permissions Allow list chips phantom.yaml:permissions.allow
Permissions Deny list chips phantom.yaml:permissions.deny

Bugs fixed

  1. permissions.defaultMode can now be unset via the UI (new schema + explicit Phantom default option).
  2. Text inputs and selects now normalize unset to null at submit time (was "" for text, undefined for select).
  3. Discard goes through a confirmation modal matching the Memory tab pattern (was a silent wipe).
  4. Audit log is rendered in a History drawer at the bottom (endpoint existed, UI was missing).
  5. Dirty state is tracked per field (was per top-level slice).
  6. Inline error slots under every field surface Zod validation errors in place with role="alert" and auto-scroll.
  7. Legacy settings-editor schema accepted 17 keys that no UI rendered; new schema is tight by construction and rejects unknown keys via .strict().

Backend contract

GET  /ui/api/phantom-config           -> { config: PhantomConfigForUi, audit: { last_modified_at, last_modified_by } }
PUT  /ui/api/phantom-config           -> partial update, atomic YAML write, one audit row per dirty field
GET  /ui/api/phantom-config/audit     -> newest-first rows, limit capped at 100

All routes behind cookie auth. Invalid method returns 405. Unknown top-level key on PUT returns 400. Out-of-range numerics return 400 with a field path. Mid-write rename failure returns 500 with the temp file cleaned up.

Test plan

  • bun test green (1,729 pass, 0 fail).
  • bun run typecheck clean.
  • bun run lint clean.
  • Config loader still reads existing phantom.yaml files unchanged; new optional sections get defaults.
  • Smoke test: GET /ui/api/phantom-config returns the full UI shape.
  • Smoke test: PUT with { name: "ghost", permissions: { allow: ["Bash(git:*)"] } } writes two audit rows (one per section) and updates phantom.yaml with all siblings preserved.
  • Browser walkthrough: load /ui/dashboard/#/settings, edit Identity.name, save, verify audit row in History drawer.
  • Browser walkthrough: edit Model+cost, save, verify live reload on next query.
  • Browser walkthrough: try negative max_budget_usd, verify inline error renders and Save stays disabled.
  • Browser walkthrough: navigate away with a dirty section, verify shared unsaved-changes prompt.
  • Browser walkthrough: dark theme, mobile (380px), keyboard tab order.

Notes

  • settings.json is no longer touched by this page. Agents that need SDK-internal knobs edit that file directly via SSH.
  • The existing /ui/api/settings endpoint and the src/settings-editor module are removed with their tests; no production consumer referenced them.
  • Every PUT writes one audit row per dirty field into settings_audit_log with a new section column (migration 46). Secrets are never logged because the schema does not accept them.

Adds PermissionsConfigSchema and EvolutionUiConfigSchema and wires both
into PhantomConfigSchema so the operator-tunable blocks live in
phantom.yaml. Both default to safe values, so existing phantom.yaml files
keep loading unchanged. Inline PhantomConfig test fixtures updated to
match the inferred type.
Adds a section TEXT column to settings_audit_log so the PR6 phantom-config
endpoint can tag rows with the six new sections (identity, model_cost,
evolution, channels, memory, permissions). Existing rows keep NULL and
render as "legacy" in the audit drawer. Migration indices assertion bumped
to include the new entry.
Replaces the UI-facing settings surface. Reads and writes config/phantom.yaml
plus the cadence overlay at phantom-config/meta/evolution.json, the channels
enable flags at config/channels.yaml, and memory context limits at
config/memory.yaml. Every write is tmp-file-plus-rename so a crash cannot
leave a torn file.

- Derives PhantomConfigForUiSchema via z.pick() off PhantomConfigSchema so the
  UI surface cannot drift from the loader.
- Uses .strict() at every level so unknown keys reject at parse; Anthropic
  keys, Slack tokens, webhook secrets, and email passwords are not part of
  the shape and cannot be written through PUT.
- Writes one settings_audit_log row per dirty field, tagged with its section
  (identity, model_cost, evolution, channels, memory, permissions).
- Test case for the mid-write crash path: a throwing rename stub leaves
  phantom.yaml byte-identical and the audit table empty.

23 new tests covering 401, GET shape, secret omission, single-field PUT,
nested permissions partial update, out-of-range validation, unknown-key
rejection, secret injection rejection, mid-write failure, no-op PUT,
evolution cadence overlay write, channel toggle with secret preservation,
memory limit write preserving the collections block, audit pagination and
ceiling, malformed yaml, and invalid JSON body.
…hemas + storage

The old /ui/api/settings endpoint and the src/settings-editor module wrote
to ~/.claude/settings.json (Agent SDK internals). Operators who need those
knobs edit the file via SSH. The new /ui/api/phantom-config endpoint replaces
it end to end with zero overlap.

Also splits phantom-config.ts so each file stays under ~250 LOC:
- phantom-config-schemas.ts owns the Zod shapes and the derived types.
- phantom-config-storage.ts owns file IO, projection to the UI shape, the
  deep-merge patcher, and the write-plan builder.
- phantom-config.ts stays as the thin handler that orchestrates them.

No production consumer referenced the deleted surface.
… fixes)

Rewrites public/dashboard/settings.js against /ui/api/phantom-config. Six
sections: Identity, Model and cost, Evolution, Channels, Memory, Permissions.
Each section has its own form state, dirty indicator, Save button, and
Discard button that routes through the shared unsaved-changes modal.

Bugs fixed (matching the audit doc):
- Permission default mode is now fully settable with a real "use Phantom
  default" option and drops the SDK-only plan / dontAsk enums.
- Text inputs and selects now submit null (not "" / undefined) for unset,
  so the audit log captures a consistent shape per field.
- Discard wraps in a confirmation modal, matching the Memory tab pattern.
- Audit log UI is live: the History drawer lazy-loads 20 newest rows on
  first expand.
- Every dirty field is tracked individually; the "N dirty" count reflects
  touched fields rather than top-level slices.
- Inline error slots under every field surface Zod validation errors in
  place, with role="alert" and auto-scroll to the first error.
- Cadence minutes get preset chips (30 / 60 / 180 / 360 / 1440) so the
  common values are one click away.

CSS: adds .dash-settings-section-actions (per-section footer),
.dash-field-error (inline error), .dash-settings-history (collapsible
history drawer with caret indicator), plus a mobile stack rule that flips
section actions to full-width below 720px.

Also updates src/agent/prompt-blocks/dashboard-awareness.ts so the agent
describes Settings as an operator-facing form over phantom.yaml with six
sections, not a curated form over settings.json.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2aed8d5f28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +233 to +236
return json({
config: after,
dirty_keys: changes.map((c) => c.field),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply saved config to the running runtime/cadence

This handler returns success after writing files/audit, but it never notifies live runtime objects about the new values. In current wiring, AgentRuntime and EvolutionCadence are initialized once at startup from disk (src/index.ts), so edits saved here (model/cost/permissions/evolution cadence) do not take effect until restart despite UI text claiming next-message/next-tick behavior. Please add a post-save apply hook (or dependency callback) so successful PUTs update in-memory runtime state.

Useful? React with 👍 / 👎.

Comment thread src/ui/api/phantom-config.ts Outdated
Comment on lines +192 to +193
const memRes = readYamlFile("config/memory.yaml");
if (!memRes.ok) return errJson(memRes.error, 500);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip memory.yaml parse for non-memory updates

handlePut always parses config/memory.yaml before computing the write plan, even when the incoming patch does not touch memory fields. If memory.yaml is malformed, unrelated saves (e.g., identity/model/permissions) will fail with 500 and become impossible through the dashboard. This parse should be conditional on memory changes (or non-memory saves should tolerate memory parse failures).

Useful? React with 👍 / 👎.

…sible, honest labels

P0 (reviewer): the Permissions section shipped with dashboard controls
that silently did nothing. runtime.ts and chat-query.ts both hardcoded
permissionMode='bypassPermissions' and ignored config.permissions.
Introduced src/agent/permission-options.ts to project the PhantomConfig
permissions block onto the SDK query() options (permissionMode, allow
tools, deny tools, skip-permissions flag). Both call sites spread it
into their options object, so Save in /ui/dashboard/#/settings now
actually changes agent behavior on the next message. The
allowDangerouslySkipPermissions flag only flips on in bypass mode; the
acceptEdits and default modes leave it off as the SDK expects.
9 unit tests cover the mapping.

P0 (reviewer, Codex): the Evolution and Model+cost sections labeled
themselves 'live on the next message' but the runtime is
constructor-snapshotted. Rather than ship a claim that does not hold,
relabel the five sections that are not live-reloadable as 'Save
(restart required)' and update their help copy to match. Permissions
keeps 'Live on the next message' because it now actually is. Full
live-reload wiring for model/effort/max_budget/memory/cadence is a
focused follow-up PR.

P2 (Codex): handlePut read config/memory.yaml on every request, even
for non-memory patches, so a malformed memory.yaml returned 500 for
identity/model/permissions saves too. Skip the read unless the patch
touches the memory section. planWrites now accepts a null
memoryBefore when the caller opted out. Two new tests cover both
paths (non-memory PUT tolerates corruption, memory PUT still 500s).

P1 (reviewer): the audit log serialized raw before/after values, so
an operator pasting 'sk-ant-api03-...' or 'xoxb-...' into a free-form
field (role, domain, name) had the literal token recorded in the
history pane. Added a defense-in-depth redactor keyed on known
secret-shape prefixes (Anthropic, OpenAI, Slack bot/user/app, GitHub
personal/user/server/refresh, Telegram bot). JSON.stringify runs
after redaction so nested fields and arrays are walked. Two new tests
assert redaction on Anthropic and Slack shapes.
@mcheemaa mcheemaa merged commit 466a768 into main Apr 17, 2026
1 check passed
mcheemaa added a commit that referenced this pull request Apr 17, 2026
Bumps the version to 0.20.0 in every place it's referenced:
- package.json (1)
- src/core/server.ts VERSION constant
- src/mcp/server.ts MCP server identity
- src/cli/index.ts phantom --version output
- README.md version + tests badges
- CLAUDE.md tagline + bun test count
- CONTRIBUTING.md test count

Tests: 1,799 pass / 10 skip / 0 fail. Typecheck and lint clean. No
0.19.1 or 1,584-tests references remain in source, docs, or badges.

v0.20 shipped eight PRs on top of v0.19.1:
  #71 entrypoint dashboard sync + / redirect + /health HTML
  #72 Sessions dashboard tab
  #73 Cost dashboard tab
  #74 Scheduler tab + create-job + Sonnet describe-assist
  #75 Evolution Phase A + Memory explorer tabs
  #76 Settings page restructure (phantom.yaml, 6 sections)
  #77 Agent avatar upload across 14 identity surfaces
  #79 Landing page redesign (hero, starter tiles, live pages list)
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