mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-11-01 19:40:35 +00:00
Closes #926. ## What changes Recurring Missions now survive OpenCode restarts (update, crash and relaunch, reboot) without a manual **Resume**, and the scheduler offers more than "once a day at a fixed time". - **Native schedules.** OpenCode's own scheduler (`experimental.schedules`, OpenCode 2.2.3+) owns the calendar; Missions decides whether a passage may run (CAS admission, no duplicates, Pause/Stop, errors and uncertain states stay manual). A dedicated technical conversation with a local no-op model carries the wake, so wakes, titles and compaction make no provider HTTP requests. - **Richer scheduling**, in one collapsed **Scheduling** section of the Create/Edit editor (default: once, immediately, no condition): date, delay, interval (≥15 min), daily, chosen weekdays, up to eight times a day, day of month, native cron; optional end date/duration and a maximum number of launched passages; five event conditions (another schedule succeeds, a one-time Mission completes, new local commits, a new message in a followed conversation, explicit files change). Up to five upcoming dates are previewed in the saved IANA zone. - **Created paused, started with Play.** Everything is editable before the first launch; after it, edits follow the approved matrix (future parameters between passages, objective/notes journaled during a passage). - **Keep OpenCode running.** A Preferences setting supervises the shared OpenCode service through the OS (Windows Task Scheduler with a periodic recovery task, macOS LaunchAgent, Linux systemd user unit with lingering, WSL from Windows). ON by default at the first Play unless OFF was saved; OFF removes supervision without stopping OpenCode; a live service is never killed or duplicated. - **Existing daily schedules** keep their path and controls; no migration. - Help (English and nine translations) and dev-docs are updated. ## Validation - Typechecks (4 workspaces); server Missions/host-lifetime suites; UI unit tests 1945/1945; help 31/31 and build. - Real isolated OpenCode services (2.2.5, minimum 2.2.3): resting schedule resumes after service restart with no client and no Resume; 32 real passages with history/quota/phase preserved; concurrent wakes admit once; DST and catch-up for every frequency type; forced local auto-compaction and restart during maintenance with zero provider HTTP; real upgrade 2.2.3 → 2.2.5. - OS supervision on Windows, WSL, Ubuntu VM (two real reboots, healthy before login; OFF without a session) and macOS VM (Aqua session, crashes, OFF/adoption). On the final bundle: two real periodic Windows recovery relaunches; WSL OFF/Retry on a live HTTP 500 service keeps the same process. - Windows Electron and Tauri packages run in private profiles: paused creation, nine frequencies, preview. - Full browser suite: 956/957 in the last tier; the failure is `directory-browser.test.ts` (folder picker timeout), unchanged by this branch, and that file passes 20/20 on rerun. Earlier dev-baseline runs on the same machine also lost module loads to `net::ERR_NO_BUFFER_SPACE`. ## Known limits (not qualified in this PR) - macOS reboot followed by a real login, and a Windows host reboot/logon (needs a disposable bench). - Real screen-reader output (keyboard, ARIA and RTL checks pass). - Linux/macOS desktop packages. - OpenCode's background *pre*-compaction never applies to the technical wake conversations: their local model reports zero tokens, below the native minimum. Their automatic local compaction is qualified. ## Follow-up Integration with native `/goal` comes in a separate PR.
21 lines
14 KiB
Markdown
21 lines
14 KiB
Markdown
# Settings and Preferences
|
|
|
|
Settings rows and actions, the native Preferences window and its project context, Providers/Web search, Stats, notifications and bug reports.
|
|
|
|
## Agent conventions
|
|
|
|
- Shared service supervision uses the authenticated `GET|POST /api/opencode/supervision` routes and `ServiceSupervisionSettings`, independently of project selection. Its resolver consumes the exact selected native or Windows-to-WSL runtime namespace; unavailable or ambiguous hosts fail closed. Controls pin service key/revision and revalidate a live human; reads are observation-only. The service-wide manifest and durable guard artifacts live outside CodeNomad profiles. `recoveryNextTickAt` is the independently verified OS Recovery date, while `retryAt` is a live guard's backoff. Frontend presentation consumes the shared API types and localized fixed cause codes. Runtime, storage and platform gates are in [OPENCODE_RUNTIME_SETUP.md](OPENCODE_RUNTIME_SETUP.md#service-supervision-primitives-and-native-adapters).
|
|
|
|
- Settings → OpenCode's `ServiceSupervisionCard` presents the shared preference with the lone-setting switch, protection state, fixed localized cause, responding-service distinction and known attempt/retry/recovery dates. Status, errors and actionable recovery information stay visible. The shared focusable **i** tip beside the label, available on hover, keyboard focus and touch, explains the service-wide scope, default-ON behavior, OFF leaving the service running, Linux lingering's effect on every enabled user service, login scope and OS sleep limits. `useServiceSupervision` reads on section demand, visibility restoration, reconnect and explicit refresh, coalesces trailing reads and fences selection/unmount responses. Passive failures retain the last successful display and disable writes until a successful read. In-flight and uncertain controls survive card remounts in per-window memory, keyed by service and exact request; status reads reconcile receipts and only an explicit original-action retry can resend. A known revision conflict triggers a read and review message. Deferred linger rollback remains visible with explicit Retry. Browser regression: `tests/browser/service-supervision.test.ts`; all ten locale cause/placeholder contracts: `src/lib/i18n/service-supervision.test.ts`.
|
|
|
|
- Settings → Providers separates Models, Web search and Images into three shared `.settings-disclosure` groups (no icons): Models starts open, Web search and Images folded, and a folded group mounts nothing, so its connection metadata, configuration contract and image-model catalog are read only once opened. Models and Web search each start with a provider selector, Connect action and configured-provider list; Web search then shows its global default and project override. Images (`opencode/image-settings.ts`, `settings/image-settings-card.tsx`, `styles/components/image-settings.css`) edits the Global document's `experimental.image_generate` and `experimental.media_blob`. OpenCode reads `image_generate` per field from the highest-priority document that sets it (unset means on, with the automatic model; only `false` turns the image tool off), but `media_blob` (like `subagent_depth`) from the whole `experimental` object of the highest-priority document that has one, so writing a project block would hide inherited experimental settings. The document that decides each value is read from the native configuration entries and reported under its control when it is not the Global one. When the selected Location's own project document (the project write target) decides `media_blob` through its existing `experimental` object, the snapshot names it under `mediaStorage.project` instead, the box shows the effective value and saving also writes `media_blob` there; the edit is refused if that object has disappeared, because creating one would hide inherited settings. The Missions depth editor's Inherit removes a project object left holding only that `media_blob` when it equals the Global one (`dev-docs/MISSIONS.md`); Off is written as `false`, because removing the key would expose a lower-priority document that enables it. When the fresh Global write target has no `experimental` object while a lower-priority document has one, creating it would hide that whole object (subagent depth, todos) from every Location without its own, even when the selected Location overrides it, so the group is read-only and the backend refuses the write; never copy the inherited values into the Global document instead. Each control appears only when the connected daemon's `Config.InfoEncoded` declares its key, because the native `experimental` object rejects unknown keys; never write either key without that capability. Image models come from native `GET /api/model?types=image` for the selected Location. Reuse provider rows, selectors, inputs and action styles; do not add an API-key disclosure or a separate card design. Search-only integrations stay out of the Models group. Web search configuration lives entirely here. Read connection metadata while this Settings surface is visible, never credential secret values; keep API-key entry explicit and clear drafts on context changes. Use the authorized Global/Project document operations shared with plugin controls and `opencode/native-setting-document.ts` for targeted JSONC edits. Preserve unrelated fields, native discovery roots, WSL atomic writes and conflict/connection/deletion fences. Group/search styling lives in `styles/components/websearch-settings.css`; credentials use native integration methods and remain global.
|
|
- Stats preferences explicitly aggregate the entire connected OpenCode service via `opencode/service-usage.ts`. Reserve the Usage label for the provider subscription panel. Native project IDs span independent clones, so never label native `session.stats` as physical-folder/Git-family scoped. Keep connection/workspace fences, `tools: "none"` and periods bounded to 366 days; never load transcripts for statistics or imply billing/quota precision. Last snapshots and period choices restore through backend-owned `state.ui.serviceUsage` (eight each/1 MiB, validated service/channel/folder identity), never a Preferences writer to main-window client state or a workspace-UUID cache. Native aggregation must not await optional discovery/cache reads; cached responses cannot replace fresh data. Backend authorized admission order, not completion order or client time, chooses the period across windows; optional publication is bounded and shutdown-fenced. Keep last-known-owner totals explicitly stale through disconnect/errors and revoke them on verified owner/discovery changes. Read while mounted and document-visible; hide cancels queued/trailing demand, activation revalidates once and terminal hints use the shared activity cooldown. Never treat document visibility as native minimize/occlusion detection or equate blur with hidden. See `dev-docs/CACHE_REFRESH_CONVENTIONS.md` for the remaining native scope. Styles live in `styles/components/usage-dashboard.css`.
|
|
- Settings rows keep only a title unless a caption is essential; secondary explanations go behind `components/settings/settings-info-tip.tsx` (a focusable "i" with a hover/focus tooltip that a tap toggles on touch screens, a span so it never becomes a row label's control or toggles it), styled in `styles/components/settings-info-tip.css`. Behavior settings opt in with `infoKey` instead of `subtitleKey`. Folded settings groups use the one `.settings-disclosure` (`styles/components/settings-disclosure.css`; a summary may hold an inline heading that keeps the summary's look): Missions gathers every agent/model choice under "Agents and models" with the current request in its summary, and Advanced ends with the environment-variable editor folded under a single title.
|
|
- Settings update/install actions (OpenCode and Info), OpenCode's post-update service restart, and Remote Control start/pairing-link actions use the shared `.selector-button-primary` accent treatment. The shared `OpenCodeSetupPanel` keeps those same primary actions in startup recovery. Remote Control's header refresh is an icon-only `.window-icon-button`, retaining its localized accessible name, tooltip, disabled state and loading spinner.
|
|
- The native Preferences window never owns client state. Its General startup card (`settings/opener-startup-state-settings-card.tsx`) targets the exact local window that opened Preferences: the host records that opener and an epoch (Electron `PreferencesWindowRegistry`, Tauri `PreferencesWindow`), forwards only the fixed commands in `lib/native/startup-state-command.ts` to that renderer, which runs them through its own `stores/client-state` transitions. Mutations carry the displayed epoch and degrade to a read after the opener changes. Startup state is per window: when the opener is unknown (Preferences restored at launch) or closed, the host adopts the most recently used local window as the opener (`startupStateTargetId` + `adoptOpener` / `startup_state_target` + `set_opener`), rotating the epoch so a change prepared for another window is only a read. The card re-reads a few times while the adopted window starts and stays disabled only when no local window can answer.
|
|
- The native Preferences window shows no session, so it never reacts to session events: `stores/sessions.ts` wires the session event handlers (errors, TUI toasts, permissions, session and worktree updates) only outside it. A session alert, toast or notice therefore appears in the windows showing sessions and is acknowledged once there (`tests/browser/preferences-window.test.ts`).
|
|
- Native Preferences resolves its project context through `settings/preferences-project-context.ts` before Providers, Missions or Stats read project data. Backend workspace IDs are ephemeral: use the exact requested record while it exists; otherwise bind only one unambiguous exact-folder match from the backend's metadata-only inventory. Requests persist the opened backend-host folder as `workspacePath`, separate from the selected native `location.directory` (WSL/worktrees); keep the native directory for requests and drop any stale native workspace selector when rebinding. Requests without the backend folder match only their exact directory, never through guessed WSL translations or ancestors. A starting, missing or ambiguous project stays inactive. Read only on section demand, matching workspace lifecycle events or reconnect; never hydrate other workspaces, discover worktrees, create projects or poll. Record all deletion tombstones before filtering by the current binding so late inventory responses cannot publish deleted candidates, and revalidate when deletion resolves an ambiguity. Invalidation and newer native requests fence late reads, explicitly deleted occurrences never turn into sibling tabs, and rebinding persists through the existing Preferences request queue without resetting the section or newer scrolling. The shared renderer and request contract cover both Electron and Tauri; startup regressions live in `tests/browser/preferences-startup.test.ts`.
|
|
- Settings → Info's Support CodeNomad card, between Updates and Diagnostics, holds one `.selector-button-secondary` action that opens `helpUrl(locale, "sponsor")` through `openExternalUrl`, like the `open-sponsor` palette command and the native Help menu item; a failed opening shows the localized `commands.openSponsor.failed` alert in the card and is never replayed. Funding channels live only on the help site (`docs/help/funding.json`); never put payment links or amounts in the app.
|
|
- Settings → Info's Report a bug opens the `bug_report.yml` issue form with its text fields prefilled by id (`BUG_REPORT_FIELDS` in `lib/bug-report.ts`). When the template changes, keep those ids and their input/textarea types in sync: `lib/bug-report.test.ts` checks the repository's template. The section warms a bounded read of the form published on the default branch; the click never waits for it (WebKit drops user activation after about 1 s), using the settled assessment or else the form while a background read retries. Only a proven incompatible form (missing, unparsable, prefilled field removed or retyped) falls back to a blank issue carrying the same Markdown report; an unreachable read keeps the form. Never prefill listeners, addresses, paths, credentials or conversation data.
|
|
- Notification icon badges mirror the existing per-window bell/history unread count, never session status or native OS notification delivery. The browser favicon uses a dot; native Electron/Tauri hosts aggregate live primary-window counts and clear contributions on reload/close, keeping preferences and preview guests outside the badge authority. Keep UI projection in `lib/notification-icon-badge.ts` and host dispatch in `lib/native/notification-badge.ts`; do not add discovery scans or polling.
|
|
- Notifications have one routing table (`lib/notification-routing.ts`): every `showToastNotification` declares a category, and Settings → Notifications chooses per category On screen / History / System (`components/settings/notification-routing-table.tsx`, `styles/components/notification-routing.css`). The history, and therefore the icon badges, holds only what is routed there; errors always reach the screen and the history; `transient` live feedback (progress, retry countdowns, "up to date") stays on screen only. The routing table is the only system-notification control: checking a System box requests permission, and saved `osNotificationsEnabled`, `notifyOnNeedsInput` and `notifyOnIdle` preferences only seed the routing until it is first saved. The bell (tab bar, or title bar in the macOS desktop app) is the history, independent of system notifications. System delivery is held back only while the window has focus (visible and `document.hasFocus()`), unless **Notify when the app is focused** (`osNotificationsAllowWhenVisible`) is on; a window left open behind another application or on another screen still notifies (`tests/browser/system-notification-focus.test.ts`). Classify new notifications by what the user would want to find later, not by their source file; a warning that must be seen once by default (the incomplete-recovery warning) uses a category that shows a bubble (`projects`), never `needsInput`, whose default is System only.
|