diff --git a/cmd/codeaf/chatv3.go b/cmd/codeaf/chatv3.go index a0443dcfc7..472c918c57 100644 --- a/cmd/codeaf/chatv3.go +++ b/cmd/codeaf/chatv3.go @@ -962,6 +962,8 @@ func openV3Launch(proc *v3Process, opts v3Options) (*v3Launch, error) { BaseURL: settings.BaseURL, Sources: settings.Sources, CompactEnabled: !opts.NoCompact, + // Carry the saved reader choice into the live chat and its tasks. + DocumentEngine: settings.DocumentEngine, SessionFile: transcript, TaskLanded: taskLanded, // The folder this conversation keeps everything in (Decision 26). It is diff --git a/cmd/codeaf/settings_document_test.go b/cmd/codeaf/settings_document_test.go new file mode 100644 index 0000000000..2cd4e180db --- /dev/null +++ b/cmd/codeaf/settings_document_test.go @@ -0,0 +1,21 @@ +package main + +import "testing" + +// The launch boundary must carry the resolved preference to read_document; +// merely saving and displaying it made the old settings row ineffective. +func TestSettingsDocumentReaderReachesLiveChat(t *testing.T) { + for _, engine := range []string{"auto", "local", "free", "ocr"} { + t.Run(engine, func(t *testing.T) { + proc := v3TestProcess(t) + proc.Settings.DocumentEngine = engine + launch, err := openV3Launch(proc, v3Options{Workspace: t.TempDir(), Model: "test/model", OneModel: true}) + if err != nil { + t.Fatal(err) + } + if launch.Config.DocumentEngine != engine { + t.Fatalf("document reader = %q, want %q", launch.Config.DocumentEngine, engine) + } + }) + } +} diff --git a/docs/changes/unreleased/1792-settings-clarity.md b/docs/changes/unreleased/1792-settings-clarity.md new file mode 100644 index 0000000000..1ea2235dfb --- /dev/null +++ b/docs/changes/unreleased/1792-settings-clarity.md @@ -0,0 +1,19 @@ +--- +kind: changed +title: Clear settings categories and AI team navigation without resetting preferences +pr: 1792 +surface: [chat, engine, docs] +invalidates: + - "Chat settings tools now discover the same names and categories as the panel, explain legacy value mappings and restart requirements, and leave resident-only controls out of ordinary chat discovery. Interface preferences refresh after panel writes and at chat turn completion." + - "Choice settings no longer silently cycle on activation. They show the saved selection and alternatives before committing; number and text fields keep clickable Save/Cancel and validation beside the draft." + - "Chat settings used ten mixed tabs. Nine purpose-based categories now use a sidebar in wide terminals, a compact bar in narrow ones, searchable advanced controls and service options." + - "Teams could imply human collaboration and Sessions obscured delegated work. The navigation now says AI teams and Activity, keeps Memory visible and preserves numbered shortcuts, typed aliases and saved visit keys." + - "Resident-only controls and unwritable legacy model slots appeared in chat settings. They are omitted from this surface while their stored values and the resident registry remain intact." + - "Hover descriptions could move controls and mouse clicks could reach behind editors. Details now occupy stable rows; editors protect their input and retain invalid values for correction." + - "The document-reader preference was saved but not carried into the live chat engine. New CLI launches now pass the resolved preference into the session and its tasks." + - "Show hints formerly appeared as a negative disable question. The positive label preserves the existing persisted preference; profile keys and unknown fields are not migrated or reset." +--- + +Settings explain scope, environment overrides and known activation boundaries. +Already-open chats still need a CLI restart for settings captured at startup, +including memory activation. The built-in manual describes those limits. diff --git a/docs/design/settings-clarity/README.md b/docs/design/settings-clarity/README.md new file mode 100644 index 0000000000..f8b6c63aa6 --- /dev/null +++ b/docs/design/settings-clarity/README.md @@ -0,0 +1,184 @@ +# Settings clarity + +A settings page should answer four questions together: what this changes, what it is set to, where it applies, and when the change takes effect. + +## Design decisions + +- Nine categories separate everyday preferences, models, memory, task behavior, AI team defaults, permissions, spending, connections, and privacy. AI teams explicitly names coordinated AI work; it does not suggest a multi-user organization. +- Wide terminals use a category sidebar; narrower terminals use a scrolling category bar. The top bar uses text in sentence case. No new font-dependent icons are required. +- Search sits above the list and covers every category, including Advanced controls and the connection catalog. Existing names and config keys still work. Model selection has a separate **Filter models** box. +- General contains **show hints**, with a positive on/off reading. Turning tips off leaves keyboard instructions and setting explanations available. Selected and hovered controls share the same help area, whose fixed height prevents rows moving under the pointer. +- Less common controls remain under Advanced. Search can find them while collapsed. Memory has a direct route to inspecting saved memories. +- Names and values describe behavior: task scope assessment, assess while working, collapsed tool details, ask by default, and use-only model recommendations. Scope and restart requirements remain explicit. +- Choice settings open a list marked with the current value; browsing or cancelling writes nothing. Number and text editors group the title, accepted format, input, validation and clickable Save/Cancel controls. A completed write shows Saved. A remote write already in flight shows Saving and Close instead of implying it can still be cancelled. +- Invalid edits retain the draft and error. Escape cancels the editor; from search it clears the query first. Hosted team edits wait for acknowledgement. + +## Compatibility and architecture + +`Setting.ChatPresentation()` supplies categories, labels, descriptions, aliases, scope and activation timing to both the TUI and the chat settings tools. `config.ChatChoiceLabel` shares readable choice names while preserving raw registry values. The existing registry remains the source of validators, writers and saved values. The UI value adapter changes readings without migrating configuration. Existing profiles retain their keys, values, unknown fields and defaults; opening settings writes nothing. + +The shared settings surface combines registry controls with live connection and model-account catalogs. It uses the existing editors, provider picker, memory view and crew panel. Moving a setting does not create another settings store. Document-reader selection now reaches the v3 session configuration, closing a consumer wiring gap found during the audit. + +Resident-only practice controls and unwritable legacy role slots are excluded from live chat settings. Their stored values remain intact. Canonical role and tier controls remain under Advanced. + +## Capture method + +These are real terminal frames, not mockups. The capture script starts the built CLI inside a private tmux server, with an isolated fixture profile, no host, automatic updates disabled, and a loopback model endpoint. No user account or model request is needed. `agg` and `ffmpeg` render the ANSI capture to PNG. Plain-text captures accompany each image for inspection. + +Before: `4fd9cfaa0fb63b1c78db5aa6ebe9479e512d1640` (dev base). After revision is recorded in [verification](verification.md). Both use 80- and 120-column terminals, 48 rows, and the same fixture. + +From a built checkout on Spark: + +```sh +python3 docs/design/settings-clarity/capture.py --binary "$(pwd)/bin/codeaf" --out /tmp/settings-proof --version after --columns 120 +python3 docs/design/settings-clarity/render.py /tmp/settings-proof +``` + +The fixture's key is dummy data. Live third-party OAuth, billing, and provider availability are not established by these screenshots. + +## Category comparisons + +Each pair below shows the primary former location and the new category. The complete baseline index follows because several old categories were combined or split. + +### General + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before General](before/04-display-120c.png) | ![After General](after/01-general-120c.png) | + +[Before · 80 columns](before/04-display-80c.png) · [After · 80 columns](after/01-general-80c.png) + +### Models + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Models](before/09-providers-120c.png) | ![After Models](after/02-models-120c.png) | + +[Before · 80 columns](before/09-providers-80c.png) · [After · 80 columns](after/02-models-80c.png) + +### Memory + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Memory](before/01-session-120c.png) | ![After Memory](after/03-memory-120c.png) | + +[Before · 80 columns](before/01-session-80c.png) · [After · 80 columns](after/03-memory-80c.png) + +### Tasks + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Tasks](before/07-tasks-120c.png) | ![After Tasks](after/04-tasks-120c.png) | + +[Before · 80 columns](before/07-tasks-80c.png) · [After · 80 columns](after/04-tasks-80c.png) + +### AI teams + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before AI teams](before/08-teams-120c.png) | ![After AI teams](after/05-ai-teams-120c.png) | + +[Before · 80 columns](before/08-teams-80c.png) · [After · 80 columns](after/05-ai-teams-80c.png) + +### Permissions + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Permissions](before/06-safety-120c.png) | ![After Permissions](after/06-permissions-120c.png) | + +[Before · 80 columns](before/06-safety-80c.png) · [After · 80 columns](after/06-permissions-80c.png) + +### Spending + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Spending](before/05-spending-120c.png) | ![After Spending](after/07-spending-120c.png) | + +[Before · 80 columns](before/05-spending-80c.png) · [After · 80 columns](after/07-spending-80c.png) + +### Connections + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Connections](before/10-connections-120c.png) | ![After Connections](after/08-connections-120c.png) | + +[Before · 80 columns](before/10-connections-80c.png) · [After · 80 columns](after/08-connections-80c.png) + +### Privacy + +| Before · 120 columns | After · 120 columns | +| --- | --- | +| ![Before Privacy](before/04-display-120c.png) | ![After Privacy](after/09-privacy-120c.png) | + +[Before · 80 columns](before/04-display-80c.png) · [After · 80 columns](after/09-privacy-80c.png) + +## All former categories + +- Session: [120 columns](before/01-session-120c.png) · [80 columns](before/01-session-80c.png) +- Context: [120 columns](before/02-context-120c.png) · [80 columns](before/02-context-80c.png) +- Workspace: [120 columns](before/03-workspace-120c.png) · [80 columns](before/03-workspace-80c.png) +- Display: [120 columns](before/04-display-120c.png) · [80 columns](before/04-display-80c.png) +- Spending: [120 columns](before/05-spending-120c.png) · [80 columns](before/05-spending-80c.png) +- Safety: [120 columns](before/06-safety-120c.png) · [80 columns](before/06-safety-80c.png) +- Tasks: [120 columns](before/07-tasks-120c.png) · [80 columns](before/07-tasks-80c.png) +- Teams: [120 columns](before/08-teams-120c.png) · [80 columns](before/08-teams-80c.png) +- Providers: [120 columns](before/09-providers-120c.png) · [80 columns](before/09-providers-80c.png) +- Connections: [120 columns](before/10-connections-120c.png) · [80 columns](before/10-connections-80c.png) + +## Advanced controls and interactions + +- 02 models advanced: [120 columns](after/02-models-advanced-120c.png) · [80 columns](after/02-models-advanced-80c.png) +- 02 models advanced end: [120 columns](after/02-models-advanced-end-120c.png) · [80 columns](after/02-models-advanced-end-80c.png) +- 04 tasks advanced: [120 columns](after/04-tasks-advanced-120c.png) · [80 columns](after/04-tasks-advanced-80c.png) +- 04 tasks advanced end: [120 columns](after/04-tasks-advanced-end-120c.png) · [80 columns](after/04-tasks-advanced-end-80c.png) +- 05 ai teams advanced: [120 columns](after/05-ai-teams-advanced-120c.png) · [80 columns](after/05-ai-teams-advanced-80c.png) +- 05 ai teams advanced end: [120 columns](after/05-ai-teams-advanced-end-120c.png) · [80 columns](after/05-ai-teams-advanced-end-80c.png) +- 06 permissions advanced: [120 columns](after/06-permissions-advanced-120c.png) · [80 columns](after/06-permissions-advanced-80c.png) +- 06 permissions advanced end: [120 columns](after/06-permissions-advanced-end-120c.png) · [80 columns](after/06-permissions-advanced-end-80c.png) +- 08 connections advanced: [120 columns](after/08-connections-advanced-120c.png) · [80 columns](after/08-connections-advanced-80c.png) +- 08 connections advanced end: [120 columns](after/08-connections-advanced-end-120c.png) · [80 columns](after/08-connections-advanced-end-80c.png) +- 09 privacy advanced: [120 columns](after/09-privacy-advanced-120c.png) · [80 columns](after/09-privacy-advanced-80c.png) +- 09 privacy advanced end: [120 columns](after/09-privacy-advanced-end-120c.png) · [80 columns](after/09-privacy-advanced-end-80c.png) +- edit hints after: [120 columns](after/edit-hints-after-120c.png) · [80 columns](after/edit-hints-after-80c.png) +- edit hints before: [120 columns](after/edit-hints-before-120c.png) · [80 columns](after/edit-hints-before-80c.png) +- edit invalid value: [120 columns](after/edit-invalid-value-120c.png) · [80 columns](after/edit-invalid-value-80c.png) +- edit limit cleared: [120 columns](after/edit-limit-cleared-120c.png) · [80 columns](after/edit-limit-cleared-80c.png) +- edit limit reopened: [120 columns](after/edit-limit-reopened-120c.png) · [80 columns](after/edit-limit-reopened-80c.png) +- edit limit saved: [120 columns](after/edit-limit-saved-120c.png) · [80 columns](after/edit-limit-saved-80c.png) +- edit limit unlimited: [120 columns](after/edit-limit-unlimited-120c.png) · [80 columns](after/edit-limit-unlimited-80c.png) +- hover category: [120 columns](after/hover-category-120c.png) +- navigation activity: [120 columns](after/navigation-activity-120c.png) · [80 columns](after/navigation-activity-80c.png) +- navigation ai teams: [120 columns](after/navigation-ai-teams-120c.png) · [80 columns](after/navigation-ai-teams-80c.png) +- search old label: [120 columns](after/search-old-label-120c.png) · [80 columns](after/search-old-label-80c.png) +- search privacy: [120 columns](after/search-privacy-120c.png) · [80 columns](after/search-privacy-80c.png) +- search service: [120 columns](after/search-service-120c.png) · [80 columns](after/search-service-80c.png) + +## Behavior verification + +See [verification.md](verification.md) for tested revisions, Spark job IDs, results, and limitations. + +## Changing a setting + +The refreshed interaction captures show the real CLI after the editing pass: + +| Choose a value | Edit a number | +| --- | --- | +| ![Current choice](after/choice-current-80c.png) | ![Number editor](after/edit-number-open-80c.png) | + +[Preview another choice](after/choice-highlighted-80c.png) · [Saved choice](after/choice-saved-80c.png) · [Reopened choice](after/choice-reopened-80c.png) · [Cancelled number edit](after/edit-number-cancelled-80c.png) · [Validation](after/edit-invalid-value-80c.png) · [Mouse save](after/edit-limit-saved-80c.png) + +The capture checks that opening, previewing and cancelling do not write, a selected +choice survives reopening, clicking Cancel preserves the old count, clicking Save +persists 4, and invalid input leaves the stored value unchanged. + +## Settings through chat + +The chat can discover preferences with the same names as the TUI, read accepted +values, and write supported preferences through the same registry. Tests exercise +read → write → read for hiding/showing hints, expanded/collapsed tool details, +and memory preferences, checking the persisted profile and restart receipts. + +The tool lists exact raw values beside readable choices, including the legacy +inverse hints row. Invalid values, protected controls and hidden resident-only +controls leave the profile unchanged. Interface preferences refresh immediately +after a panel save and at chat turn completion. diff --git a/docs/design/settings-clarity/after/01-general-120c.png b/docs/design/settings-clarity/after/01-general-120c.png new file mode 100644 index 0000000000..a780540f07 Binary files /dev/null and b/docs/design/settings-clarity/after/01-general-120c.png differ diff --git a/docs/design/settings-clarity/after/01-general-120c.txt b/docs/design/settings-clarity/after/01-general-120c.txt new file mode 100644 index 0000000000..afbec696c8 --- /dev/null +++ b/docs/design/settings-clarity/after/01-general-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General General + Models + Memory › Search all settings + Tasks mouse interaction on + AI teams timestamps footers + Permissions completed tool details collapsed + Spending tool icons auto + Connections automatic updates on + Privacy background reminders off + task sidebar on + switch chats immediately on + show hints on + include model in commit attribution on + + + + + + + + + + + + + + + + + + + + + + + + on gives hover and click; off gives the terminal's own text selection back. + + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/01-general-80c.png b/docs/design/settings-clarity/after/01-general-80c.png new file mode 100644 index 0000000000..d9ae801f9f Binary files /dev/null and b/docs/design/settings-clarity/after/01-general-80c.png differ diff --git a/docs/design/settings-clarity/after/01-general-80c.txt b/docs/design/settings-clarity/after/01-general-80c.txt new file mode 100644 index 0000000000..697c135d03 --- /dev/null +++ b/docs/design/settings-clarity/after/01-general-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + mouse interaction on + timestamps footers + completed tool details collapsed + tool icons auto + automatic updates on + background reminders off + task sidebar on + switch chats immediately on + show hints on + include model in commit attribution on + + + + + + + + + + + + + + + + + + + + + + + + on gives hover and click; off gives the terminal's own text selection back. + + + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/02-models-120c.png b/docs/design/settings-clarity/after/02-models-120c.png new file mode 100644 index 0000000000..b19d0df048 Binary files /dev/null and b/docs/design/settings-clarity/after/02-models-120c.png differ diff --git a/docs/design/settings-clarity/after/02-models-120c.txt b/docs/design/settings-clarity/after/02-models-120c.txt new file mode 100644 index 0000000000..61b11bdb7f --- /dev/null +++ b/docs/design/settings-clarity/after/02-models-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models + Models + Memory › Search all settings + Tasks chat model deepseek/deepseek-v4.1-flash + AI teams image generation automatic + Permissions speech generation automatic + Spending music generation automatic + Connections video generation automatic + Privacy audio transcription automatic + image understanding model automatic + reasoning effort auto + document reader auto + fallback models nearest in the catalog + Advanced show + + + + + + + + + + + + + + + + + + + + + + + the model you are talking to. Everything below it is a model codeaf uses on your behalf. + + + + + +─ This chat and the profile default ──────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/02-models-80c.png b/docs/design/settings-clarity/after/02-models-80c.png new file mode 100644 index 0000000000..8ef340b725 Binary files /dev/null and b/docs/design/settings-clarity/after/02-models-80c.png differ diff --git a/docs/design/settings-clarity/after/02-models-80c.txt b/docs/design/settings-clarity/after/02-models-80c.txt new file mode 100644 index 0000000000..9d849c6abc --- /dev/null +++ b/docs/design/settings-clarity/after/02-models-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + chat model deepseek/deepseek-v4.1-flash + image generation automatic + speech generation automatic + music generation automatic + video generation automatic + audio transcription automatic + image understanding model automatic + reasoning effort auto + document reader auto + fallback models nearest in the catalog + Advanced show + + + + + + + + + + + + + + + + + + + + + + + the model you are talking to. Everything below it is a model codeaf uses on + your behalf. + + + + +─ This chat and the profile default ──────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/02-models-advanced-120c.png b/docs/design/settings-clarity/after/02-models-advanced-120c.png new file mode 100644 index 0000000000..a50d3a703b Binary files /dev/null and b/docs/design/settings-clarity/after/02-models-advanced-120c.png differ diff --git a/docs/design/settings-clarity/after/02-models-advanced-120c.txt b/docs/design/settings-clarity/after/02-models-advanced-120c.txt new file mode 100644 index 0000000000..bd31cabd89 --- /dev/null +++ b/docs/design/settings-clarity/after/02-models-advanced-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models + Models + Memory › Search all settings + Tasks chat model deepseek/deepseek-v4.1-flash + AI teams image generation automatic + Permissions speech generation automatic + Spending music generation automatic + Connections video generation automatic + Privacy audio transcription automatic + image understanding model automatic + reasoning effort auto + document reader auto + fallback models nearest in the catalog + Advanced hide + + routing + model provider auto + speed guard on + routing simple + prompt profile auto + reflex • deepseek/deepseek-v4.1-flash + small work • deepseek/deepseek-v4.1-flash + seats auto · 3 pinned · models all · 1 of 1 providers · per task $5 + pinned roles none + + roles · reflex + reflex deepseek/deepseek-v4.1-flash + + roles · small work + caption deepseek/deepseek-v4.1-flash + consolidate deepseek/deepseek-v4.1-flash + guardian deepseek/deepseek-v4.1-flash + intake deepseek/deepseek-v4.1-flash + jobname deepseek/deepseek-v4.1-flash + router deepseek/deepseek-v4.1-flash + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/02-models-advanced-80c.png b/docs/design/settings-clarity/after/02-models-advanced-80c.png new file mode 100644 index 0000000000..0255f1b345 Binary files /dev/null and b/docs/design/settings-clarity/after/02-models-advanced-80c.png differ diff --git a/docs/design/settings-clarity/after/02-models-advanced-80c.txt b/docs/design/settings-clarity/after/02-models-advanced-80c.txt new file mode 100644 index 0000000000..9e372e0fe3 --- /dev/null +++ b/docs/design/settings-clarity/after/02-models-advanced-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + chat model deepseek/deepseek-v4.1-flash + image generation automatic + speech generation automatic + music generation automatic + video generation automatic + audio transcription automatic + image understanding model automatic + reasoning effort auto + document reader auto + fallback models nearest in the catalog + Advanced hide + + routing + model provider auto + speed guard on + routing simple + prompt profile auto + reflex • deepseek/deepseek-v4.1-flash + small work • deepseek/deepseek-v4.1-flash + seats auto · 3 pinned · models all · 1 of 1 providers · per task $5 + pinned roles none + + roles · reflex + reflex deepseek/deepseek-v4.1-flash + + roles · small work + caption deepseek/deepseek-v4.1-flash + consolidate deepseek/deepseek-v4.1-flash + guardian deepseek/deepseek-v4.1-flash + intake deepseek/deepseek-v4.1-flash + jobname deepseek/deepseek-v4.1-flash + router deepseek/deepseek-v4.1-flash + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · tab next place · esc close diff --git a/docs/design/settings-clarity/after/02-models-advanced-end-120c.png b/docs/design/settings-clarity/after/02-models-advanced-end-120c.png new file mode 100644 index 0000000000..f4c4f7e989 Binary files /dev/null and b/docs/design/settings-clarity/after/02-models-advanced-end-120c.png differ diff --git a/docs/design/settings-clarity/after/02-models-advanced-end-120c.txt b/docs/design/settings-clarity/after/02-models-advanced-end-120c.txt new file mode 100644 index 0000000000..5f70edc7f6 --- /dev/null +++ b/docs/design/settings-clarity/after/02-models-advanced-end-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models + Models + Memory › Search all settings + Tasks consolidate deepseek/deepseek-v4.1-flash + AI teams guardian deepseek/deepseek-v4.1-flash + Permissions intake deepseek/deepseek-v4.1-flash + Spending jobname deepseek/deepseek-v4.1-flash + Connections router deepseek/deepseek-v4.1-flash + Privacy sentinel deepseek/deepseek-v4.1-flash + spellout deepseek/deepseek-v4.1-flash + taskname deepseek/deepseek-v4.1-flash + title deepseek/deepseek-v4.1-flash + + roles · worker + worker deepseek/deepseek-v4.1-flash + + roles · checker + checker deepseek/deepseek-v4.1-flash + careful deepseek/deepseek-v4.1-flash + repair deepseek/deepseek-v4.1-flash + shaper deepseek/deepseek-v4.1-flash + vision deepseek/deepseek-v4.1-flash + + roles · planner + designer deepseek/deepseek-v4.1-flash + division deepseek/deepseek-v4.1-flash + handoff deepseek/deepseek-v4.1-flash + markreader deepseek/deepseek-v4.1-flash + planner deepseek/deepseek-v4.1-flash + routerconfirm deepseek/deepseek-v4.1-flash + compact context at 60% + tokens reserved for replies 65536 tok + working context limit 160000 tok + context reuse limit 250% + reply guard on + + Restart the CLI to apply this to already-open chats + on cuts a reply that has come apart — one line or one letter repeated, alphabets mixed inside + words — throws it away and asks once more. Code blocks are never judged. + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/02-models-advanced-end-80c.png b/docs/design/settings-clarity/after/02-models-advanced-end-80c.png new file mode 100644 index 0000000000..927f669676 Binary files /dev/null and b/docs/design/settings-clarity/after/02-models-advanced-end-80c.png differ diff --git a/docs/design/settings-clarity/after/02-models-advanced-end-80c.txt b/docs/design/settings-clarity/after/02-models-advanced-end-80c.txt new file mode 100644 index 0000000000..e6a2dea349 --- /dev/null +++ b/docs/design/settings-clarity/after/02-models-advanced-end-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + consolidate deepseek/deepseek-v4.1-flash + guardian deepseek/deepseek-v4.1-flash + intake deepseek/deepseek-v4.1-flash + jobname deepseek/deepseek-v4.1-flash + router deepseek/deepseek-v4.1-flash + sentinel deepseek/deepseek-v4.1-flash + spellout deepseek/deepseek-v4.1-flash + taskname deepseek/deepseek-v4.1-flash + title deepseek/deepseek-v4.1-flash + + roles · worker + worker deepseek/deepseek-v4.1-flash + + roles · checker + checker deepseek/deepseek-v4.1-flash + careful deepseek/deepseek-v4.1-flash + repair deepseek/deepseek-v4.1-flash + shaper deepseek/deepseek-v4.1-flash + vision deepseek/deepseek-v4.1-flash + + roles · planner + designer deepseek/deepseek-v4.1-flash + division deepseek/deepseek-v4.1-flash + handoff deepseek/deepseek-v4.1-flash + markreader deepseek/deepseek-v4.1-flash + planner deepseek/deepseek-v4.1-flash + routerconfirm deepseek/deepseek-v4.1-flash + compact context at 60% + tokens reserved for replies 65536 tok + working context limit 160000 tok + context reuse limit 250% + reply guard on + + Restart the CLI to apply this to already-open chats + on cuts a reply that has come apart — one line or one letter repeated, + alphabets mixed inside words — throws it away and asks once more. Code + blocks are never judged. + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/03-memory-120c.png b/docs/design/settings-clarity/after/03-memory-120c.png new file mode 100644 index 0000000000..fe84df320b Binary files /dev/null and b/docs/design/settings-clarity/after/03-memory-120c.png differ diff --git a/docs/design/settings-clarity/after/03-memory-120c.txt b/docs/design/settings-clarity/after/03-memory-120c.txt new file mode 100644 index 0000000000..a34a5aafee --- /dev/null +++ b/docs/design/settings-clarity/after/03-memory-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Memory + Models + Memory › Search all settings + Tasks View saved memories open + AI teams remember across chats on + Permissions + Spending + Connections + Privacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inspect, correct or remove saved memories. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter open · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/03-memory-80c.png b/docs/design/settings-clarity/after/03-memory-80c.png new file mode 100644 index 0000000000..ed6e334c7a Binary files /dev/null and b/docs/design/settings-clarity/after/03-memory-80c.png differ diff --git a/docs/design/settings-clarity/after/03-memory-80c.txt b/docs/design/settings-clarity/after/03-memory-80c.txt new file mode 100644 index 0000000000..a6dc59a89f --- /dev/null +++ b/docs/design/settings-clarity/after/03-memory-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + View saved memories open + remember across chats on + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inspect, correct or remove saved memories. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter open · tab next place · esc close diff --git a/docs/design/settings-clarity/after/04-tasks-120c.png b/docs/design/settings-clarity/after/04-tasks-120c.png new file mode 100644 index 0000000000..d57e2c7b6c Binary files /dev/null and b/docs/design/settings-clarity/after/04-tasks-120c.png differ diff --git a/docs/design/settings-clarity/after/04-tasks-120c.txt b/docs/design/settings-clarity/after/04-tasks-120c.txt new file mode 100644 index 0000000000..3cebe9bb96 --- /dev/null +++ b/docs/design/settings-clarity/after/04-tasks-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Tasks + Models + Memory › Search all settings + Tasks concurrent tasks no limit + AI teams task model the crew's worker + Permissions review task results on + Spending when task results need a decision ask me + Connections repair attempts 1 + Privacy Advanced show + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are + the real ceilings. + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/04-tasks-80c.png b/docs/design/settings-clarity/after/04-tasks-80c.png new file mode 100644 index 0000000000..00a3f9631b Binary files /dev/null and b/docs/design/settings-clarity/after/04-tasks-80c.png differ diff --git a/docs/design/settings-clarity/after/04-tasks-80c.txt b/docs/design/settings-clarity/after/04-tasks-80c.txt new file mode 100644 index 0000000000..4add4d93e3 --- /dev/null +++ b/docs/design/settings-clarity/after/04-tasks-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + concurrent tasks no limit + task model the crew's worker + review task results on + when task results need a decision ask me + repair attempts 1 + Advanced show + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-120c.png b/docs/design/settings-clarity/after/04-tasks-advanced-120c.png new file mode 100644 index 0000000000..68ef1a815c Binary files /dev/null and b/docs/design/settings-clarity/after/04-tasks-advanced-120c.png differ diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-120c.txt b/docs/design/settings-clarity/after/04-tasks-advanced-120c.txt new file mode 100644 index 0000000000..45b4fd4627 --- /dev/null +++ b/docs/design/settings-clarity/after/04-tasks-advanced-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Tasks + Models + Memory › Search all settings + Tasks concurrent tasks no limit + AI teams task model the crew's worker + Permissions review task results on + Spending when task results need a decision ask me + Connections repair attempts 1 + Privacy Advanced hide + background shell commands after 30s + task scope assessment assess while working + maximum load per CPU core 1.5 per core + minimum available RAM 1536 MB + + + + + + + + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-80c.png b/docs/design/settings-clarity/after/04-tasks-advanced-80c.png new file mode 100644 index 0000000000..8c94d36d76 Binary files /dev/null and b/docs/design/settings-clarity/after/04-tasks-advanced-80c.png differ diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-80c.txt b/docs/design/settings-clarity/after/04-tasks-advanced-80c.txt new file mode 100644 index 0000000000..044ffa2f46 --- /dev/null +++ b/docs/design/settings-clarity/after/04-tasks-advanced-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + concurrent tasks no limit + task model the crew's worker + review task results on + when task results need a decision ask me + repair attempts 1 + Advanced hide + background shell commands after 30s + task scope assessment assess while working + maximum load per CPU core 1.5 per core + minimum available RAM 1536 MB + + + + + + + + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · tab next place · esc close diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-end-120c.png b/docs/design/settings-clarity/after/04-tasks-advanced-end-120c.png new file mode 100644 index 0000000000..df9fdf7233 Binary files /dev/null and b/docs/design/settings-clarity/after/04-tasks-advanced-end-120c.png differ diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-end-120c.txt b/docs/design/settings-clarity/after/04-tasks-advanced-end-120c.txt new file mode 100644 index 0000000000..70bb847e8b --- /dev/null +++ b/docs/design/settings-clarity/after/04-tasks-advanced-end-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Tasks + Models + Memory › Search all settings + Tasks concurrent tasks no limit + AI teams task model the crew's worker + Permissions review task results on + Spending when task results need a decision ask me + Connections repair attempts 1 + Privacy Advanced hide + background shell commands after 30s + task scope assessment assess while working + maximum load per CPU core 1.5 per core + minimum available RAM 1536 MB + + + + + + + + + + + + + + + + + + + + + + + + MB of memory that must be free before another task starts. 0 stops watching. + + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-end-80c.png b/docs/design/settings-clarity/after/04-tasks-advanced-end-80c.png new file mode 100644 index 0000000000..9125c7476c Binary files /dev/null and b/docs/design/settings-clarity/after/04-tasks-advanced-end-80c.png differ diff --git a/docs/design/settings-clarity/after/04-tasks-advanced-end-80c.txt b/docs/design/settings-clarity/after/04-tasks-advanced-end-80c.txt new file mode 100644 index 0000000000..a28806cb09 --- /dev/null +++ b/docs/design/settings-clarity/after/04-tasks-advanced-end-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + concurrent tasks no limit + task model the crew's worker + review task results on + when task results need a decision ask me + repair attempts 1 + Advanced hide + background shell commands after 30s + task scope assessment assess while working + maximum load per CPU core 1.5 per core + minimum available RAM 1536 MB + + + + + + + + + + + + + + + + + + + + + + + + MB of memory that must be free before another task starts. 0 stops watching. + + + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/05-ai-teams-120c.png b/docs/design/settings-clarity/after/05-ai-teams-120c.png new file mode 100644 index 0000000000..44504c6404 Binary files /dev/null and b/docs/design/settings-clarity/after/05-ai-teams-120c.png differ diff --git a/docs/design/settings-clarity/after/05-ai-teams-120c.txt b/docs/design/settings-clarity/after/05-ai-teams-120c.txt new file mode 100644 index 0000000000..bfbcbb0d82 --- /dev/null +++ b/docs/design/settings-clarity/after/05-ai-teams-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General AI teams + Models + Memory › Search all settings + Tasks ask the AI manager first on + AI teams run when team messages arrive on + Permissions daily spending limit no cap + Spending Advanced show + Connections + Privacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + a member's clarifying question goes to its manager first; you are asked only what no manager can + answer. Permission prompts always come to you. + + + + +─ Default for AI teams; teams can override it ────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/05-ai-teams-80c.png b/docs/design/settings-clarity/after/05-ai-teams-80c.png new file mode 100644 index 0000000000..2362736106 Binary files /dev/null and b/docs/design/settings-clarity/after/05-ai-teams-80c.png differ diff --git a/docs/design/settings-clarity/after/05-ai-teams-80c.txt b/docs/design/settings-clarity/after/05-ai-teams-80c.txt new file mode 100644 index 0000000000..40c5c7ffe6 --- /dev/null +++ b/docs/design/settings-clarity/after/05-ai-teams-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + ask the AI manager first on + run when team messages arrive on + daily spending limit no cap + Advanced show + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + a member's clarifying question goes to its manager first; you are asked only + what no manager can answer. Permission prompts always come to you. + + + + +─ Default for AI teams; teams can override it ────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-120c.png b/docs/design/settings-clarity/after/05-ai-teams-advanced-120c.png new file mode 100644 index 0000000000..a9740871e1 Binary files /dev/null and b/docs/design/settings-clarity/after/05-ai-teams-advanced-120c.png differ diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-120c.txt b/docs/design/settings-clarity/after/05-ai-teams-advanced-120c.txt new file mode 100644 index 0000000000..01c52c317a --- /dev/null +++ b/docs/design/settings-clarity/after/05-ai-teams-advanced-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General AI teams + Models + Memory › Search all settings + Tasks ask the AI manager first on + AI teams run when team messages arrive on + Permissions daily spending limit no cap + Spending Advanced hide + Connections maximum team nesting 3 levels + Privacy new subteam budget share 50% + + + + + + + + + + + + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-80c.png b/docs/design/settings-clarity/after/05-ai-teams-advanced-80c.png new file mode 100644 index 0000000000..a7832db4ed Binary files /dev/null and b/docs/design/settings-clarity/after/05-ai-teams-advanced-80c.png differ diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-80c.txt b/docs/design/settings-clarity/after/05-ai-teams-advanced-80c.txt new file mode 100644 index 0000000000..d5c809412b --- /dev/null +++ b/docs/design/settings-clarity/after/05-ai-teams-advanced-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + ask the AI manager first on + run when team messages arrive on + daily spending limit no cap + Advanced hide + maximum team nesting 3 levels + new subteam budget share 50% + + + + + + + + + + + + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · tab next place · esc close diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-end-120c.png b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-120c.png new file mode 100644 index 0000000000..403bff0eb9 Binary files /dev/null and b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-120c.png differ diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-end-120c.txt b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-120c.txt new file mode 100644 index 0000000000..485a1bea47 --- /dev/null +++ b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General AI teams + Models + Memory › Search all settings + Tasks ask the AI manager first on + AI teams run when team messages arrive on + Permissions daily spending limit no cap + Spending Advanced hide + Connections maximum team nesting 3 levels + Privacy new subteam budget share 50% + + + + + + + + + + + + + + + + + + + + + + + + + + + + Applies to newly created subteams only + the share of its parent's cap a new sub-team starts with. Teams that already exist keep theirs. + + + + +─ Default for AI teams; teams can override it ────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-end-80c.png b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-80c.png new file mode 100644 index 0000000000..a78428990b Binary files /dev/null and b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-80c.png differ diff --git a/docs/design/settings-clarity/after/05-ai-teams-advanced-end-80c.txt b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-80c.txt new file mode 100644 index 0000000000..2717f50d20 --- /dev/null +++ b/docs/design/settings-clarity/after/05-ai-teams-advanced-end-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + ask the AI manager first on + run when team messages arrive on + daily spending limit no cap + Advanced hide + maximum team nesting 3 levels + new subteam budget share 50% + + + + + + + + + + + + + + + + + + + + + + + + + + + + Applies to newly created subteams only + the share of its parent's cap a new sub-team starts with. Teams that already + exist keep theirs. + + + +─ Default for AI teams; teams can override it ────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/06-permissions-120c.png b/docs/design/settings-clarity/after/06-permissions-120c.png new file mode 100644 index 0000000000..d30e490ce9 Binary files /dev/null and b/docs/design/settings-clarity/after/06-permissions-120c.png differ diff --git a/docs/design/settings-clarity/after/06-permissions-120c.txt b/docs/design/settings-clarity/after/06-permissions-120c.txt new file mode 100644 index 0000000000..969a5b17bb --- /dev/null +++ b/docs/design/settings-clarity/after/06-permissions-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Permissions + Models + Memory › Search all settings + Tasks tool approvals allow by default + AI teams tool exceptions none + Permissions shell command rules none + Spending AI approval screening off + Connections start proposed tasks after 15s + Privacy Advanced show + + + + + + + + + + + + + + + + + + + + + + + + + + + + Allow by default runs tools without asking. Ask by default requests approval except for safe + read-only calls and tools that ask for permission themselves. Block by default refuses tools. + Tool exceptions override this choice; dangerous shell commands still require approval. + + + +─ Profile default; a project can override it ─────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/06-permissions-80c.png b/docs/design/settings-clarity/after/06-permissions-80c.png new file mode 100644 index 0000000000..727c3c44a0 Binary files /dev/null and b/docs/design/settings-clarity/after/06-permissions-80c.png differ diff --git a/docs/design/settings-clarity/after/06-permissions-80c.txt b/docs/design/settings-clarity/after/06-permissions-80c.txt new file mode 100644 index 0000000000..7d0e8c65fc --- /dev/null +++ b/docs/design/settings-clarity/after/06-permissions-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + tool approvals allow by default + tool exceptions none + shell command rules none + AI approval screening off + start proposed tasks after 15s + Advanced show + + + + + + + + + + + + + + + + + + + + + + + + + + + + Allow by default runs tools without asking. Ask by default requests approval + except for safe read-only calls and tools that ask for permission + themselves. Block by default refuses tools. Tool exceptions override this + choice; dangerous shell commands still require approval. + + +─ Profile default; a project can override it ─────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-120c.png b/docs/design/settings-clarity/after/06-permissions-advanced-120c.png new file mode 100644 index 0000000000..b2d2da5ddf Binary files /dev/null and b/docs/design/settings-clarity/after/06-permissions-advanced-120c.png differ diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-120c.txt b/docs/design/settings-clarity/after/06-permissions-advanced-120c.txt new file mode 100644 index 0000000000..013d845a4f --- /dev/null +++ b/docs/design/settings-clarity/after/06-permissions-advanced-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Permissions + Models + Memory › Search all settings + Tasks tool approvals allow by default + AI teams tool exceptions none + Permissions shell command rules none + Spending AI approval screening off + Connections start proposed tasks after 15s + Privacy Advanced hide + pause approval timer after 10s + + questions while you are away + permission ask me + choice ask me + judgement ask me + clarification ask me · never runs on a clock + confirmation ask me · destructive always asks + landing ask me + assumption ask me + ratify ask me + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-80c.png b/docs/design/settings-clarity/after/06-permissions-advanced-80c.png new file mode 100644 index 0000000000..388961161d Binary files /dev/null and b/docs/design/settings-clarity/after/06-permissions-advanced-80c.png differ diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-80c.txt b/docs/design/settings-clarity/after/06-permissions-advanced-80c.txt new file mode 100644 index 0000000000..f9a2ad6428 --- /dev/null +++ b/docs/design/settings-clarity/after/06-permissions-advanced-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + tool approvals allow by default + tool exceptions none + shell command rules none + AI approval screening off + start proposed tasks after 15s + Advanced hide + pause approval timer after 10s + + questions while you are away + permission ask me + choice ask me + judgement ask me + clarification ask me · never runs on a clock + confirmation ask me · destructive always asks + landing ask me + assumption ask me + ratify ask me + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · tab next place · esc close diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-end-120c.png b/docs/design/settings-clarity/after/06-permissions-advanced-end-120c.png new file mode 100644 index 0000000000..2cc076c301 Binary files /dev/null and b/docs/design/settings-clarity/after/06-permissions-advanced-end-120c.png differ diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-end-120c.txt b/docs/design/settings-clarity/after/06-permissions-advanced-end-120c.txt new file mode 100644 index 0000000000..e3ad4ede32 --- /dev/null +++ b/docs/design/settings-clarity/after/06-permissions-advanced-end-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Permissions + Models + Memory › Search all settings + Tasks tool approvals allow by default + AI teams tool exceptions none + Permissions shell command rules none + Spending AI approval screening off + Connections start proposed tasks after 15s + Privacy Advanced hide + pause approval timer after 10s + + questions while you are away + permission ask me + choice ask me + judgement ask me + clarification ask me · never runs on a clock + confirmation ask me · destructive always asks + landing ask me + assumption ask me + ratify ask me + + + + + + + + + + + + + + + + + what happens to a question of this shape when nobody answers it · kept for this project · + /autonomy ask · recommend · decide + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-end-80c.png b/docs/design/settings-clarity/after/06-permissions-advanced-end-80c.png new file mode 100644 index 0000000000..1a42c9fbb6 Binary files /dev/null and b/docs/design/settings-clarity/after/06-permissions-advanced-end-80c.png differ diff --git a/docs/design/settings-clarity/after/06-permissions-advanced-end-80c.txt b/docs/design/settings-clarity/after/06-permissions-advanced-end-80c.txt new file mode 100644 index 0000000000..e95d204dd0 --- /dev/null +++ b/docs/design/settings-clarity/after/06-permissions-advanced-end-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + tool approvals allow by default + tool exceptions none + shell command rules none + AI approval screening off + start proposed tasks after 15s + Advanced hide + pause approval timer after 10s + + questions while you are away + permission ask me + choice ask me + judgement ask me + clarification ask me · never runs on a clock + confirmation ask me · destructive always asks + landing ask me + assumption ask me + ratify ask me + + + + + + + + + + + + + + + + + what happens to a question of this shape when nobody answers it · kept for + this project · /autonomy ask · recommend · decide + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/07-spending-120c.png b/docs/design/settings-clarity/after/07-spending-120c.png new file mode 100644 index 0000000000..86d33a3e48 Binary files /dev/null and b/docs/design/settings-clarity/after/07-spending-120c.png differ diff --git a/docs/design/settings-clarity/after/07-spending-120c.txt b/docs/design/settings-clarity/after/07-spending-120c.txt new file mode 100644 index 0000000000..1ea874eb9f --- /dev/null +++ b/docs/design/settings-clarity/after/07-spending-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Spending + Models + Memory › Search all settings + Tasks daily spending limit $500 + AI teams per-chat spending limit no limit + Permissions ask before a plan costs more than asks first above $100 + Spending per task $5 a task · set in /crew · it also spends against the day and this conversation + Connections per standing run $5 a firing · each order may name its own + Privacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + what codeaf may spend on your work in a day. When the day's calls reach it, new work waits for + midnight or for you to raise it here. none removes the limit. + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/07-spending-80c.png b/docs/design/settings-clarity/after/07-spending-80c.png new file mode 100644 index 0000000000..2424de3e3e Binary files /dev/null and b/docs/design/settings-clarity/after/07-spending-80c.png differ diff --git a/docs/design/settings-clarity/after/07-spending-80c.txt b/docs/design/settings-clarity/after/07-spending-80c.txt new file mode 100644 index 0000000000..ea9918bfe7 --- /dev/null +++ b/docs/design/settings-clarity/after/07-spending-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › Search all settings + daily spending limit $500 + per-chat spending limit no limit + ask before a plan costs more than asks first above $100 + per task $5 a task · set in /crew + per standing run $5 a firing · each order may name its own + + + + + + + + + + + + + + + + + + + + + + + + + + + + + what codeaf may spend on your work in a day. When the day's calls reach it, + new work waits for midnight or for you to raise it here. none removes the + limit. + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/08-connections-120c.png b/docs/design/settings-clarity/after/08-connections-120c.png new file mode 100644 index 0000000000..8bcbdbd4cf Binary files /dev/null and b/docs/design/settings-clarity/after/08-connections-120c.png differ diff --git a/docs/design/settings-clarity/after/08-connections-120c.txt b/docs/design/settings-clarity/after/08-connections-120c.txt new file mode 100644 index 0000000000..6fba267a8d --- /dev/null +++ b/docs/design/settings-clarity/after/08-connections-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Connections + Models + Memory › Search all settings + Tasks web search provider auto + AI teams openrouter key • ••••••••used + Permissions + add a provider an OpenAI-compatible base URL · a name of your own + Spending exa key not set + Connections firecrawl key not set + Privacy jina key not set + Advanced show + + providers + · DeepSeek key + · Z.ai region · key + · Moonshot region · key + · MiniMax key + · Alibaba Qwen region · key + · Codex browser + · Ollama + · Custom OpenAI-compatible API address · key + + analytics + · Amplitude key + · ChartMogul key + · Dovetail key + · Geckoboard key + · Mixpanel key + · PostHog sign in + · Segment key + + billing + · Braintree key + · Chargebee key + · ChargeOver key + · FastSpring key + + Applies on the next web search + now firecrawl, keyless — set search.exaKey or search.firecrawlKey to raise it + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ categories · enter act · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/08-connections-80c.png b/docs/design/settings-clarity/after/08-connections-80c.png new file mode 100644 index 0000000000..7ac46120b8 Binary files /dev/null and b/docs/design/settings-clarity/after/08-connections-80c.png differ diff --git a/docs/design/settings-clarity/after/08-connections-80c.txt b/docs/design/settings-clarity/after/08-connections-80c.txt new file mode 100644 index 0000000000..a802ada1e6 --- /dev/null +++ b/docs/design/settings-clarity/after/08-connections-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › Search all settings + web search provider auto + openrouter key • ••••••••used + + add a provider an OpenAI-compatible base URL · a name of your own + exa key not set + firecrawl key not set + jina key not set + Advanced show + + providers + · DeepSeek key + · Z.ai region · key + · Moonshot region · key + · MiniMax key + · Alibaba Qwen region · key + · Codex browser + · Ollama + · Custom OpenAI-compatible API address · key + + analytics + · Amplitude key + · ChartMogul key + · Dovetail key + · Geckoboard key + · Mixpanel key + · PostHog sign in + · Segment key + + billing + · Braintree key + · Chargebee key + · ChargeOver key + · FastSpring key + + Applies on the next web search + now firecrawl, keyless — set search.exaKey or search.firecrawlKey to raise + it + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ move · ←→ categories · enter act · tab next place · esc close diff --git a/docs/design/settings-clarity/after/08-connections-advanced-120c.png b/docs/design/settings-clarity/after/08-connections-advanced-120c.png new file mode 100644 index 0000000000..d8363c5522 Binary files /dev/null and b/docs/design/settings-clarity/after/08-connections-advanced-120c.png differ diff --git a/docs/design/settings-clarity/after/08-connections-advanced-120c.txt b/docs/design/settings-clarity/after/08-connections-advanced-120c.txt new file mode 100644 index 0000000000..dd20536cbc --- /dev/null +++ b/docs/design/settings-clarity/after/08-connections-advanced-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Connections + Models + Memory › Search all settings + Tasks web search provider auto + AI teams openrouter key • ••••••••used + Permissions + add a provider an OpenAI-compatible base URL · a name of your own + Spending exa key not set + Connections firecrawl key not set + Privacy jina key not set + Advanced hide + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + SSH connection reuse 300s + SSH keepalive interval 3s + SSH missed keepalives 3 + SSH traffic priority lowdelay + + providers + · DeepSeek key + · Z.ai region · key + · Moonshot region · key + · MiniMax key + · Alibaba Qwen region · key + · Codex browser + · Ollama + · Custom OpenAI-compatible API address · key + + analytics + · Amplitude key + · ChartMogul key + · Dovetail key + · Geckoboard key + · Mixpanel key + · PostHog sign in + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ categories · enter act · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/08-connections-advanced-80c.png b/docs/design/settings-clarity/after/08-connections-advanced-80c.png new file mode 100644 index 0000000000..7a1e9dde7d Binary files /dev/null and b/docs/design/settings-clarity/after/08-connections-advanced-80c.png differ diff --git a/docs/design/settings-clarity/after/08-connections-advanced-80c.txt b/docs/design/settings-clarity/after/08-connections-advanced-80c.txt new file mode 100644 index 0000000000..0f62e3ed13 --- /dev/null +++ b/docs/design/settings-clarity/after/08-connections-advanced-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › Search all settings + web search provider auto + openrouter key • ••••••••used + + add a provider an OpenAI-compatible base URL · a name of your own + exa key not set + firecrawl key not set + jina key not set + Advanced hide + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + SSH connection reuse 300s + SSH keepalive interval 3s + SSH missed keepalives 3 + SSH traffic priority lowdelay + + providers + · DeepSeek key + · Z.ai region · key + · Moonshot region · key + · MiniMax key + · Alibaba Qwen region · key + · Codex browser + · Ollama + · Custom OpenAI-compatible API address · key + + analytics + · Amplitude key + · ChartMogul key + · Dovetail key + · Geckoboard key + · Mixpanel key + · PostHog sign in + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ categories · enter act · tab next place · esc close diff --git a/docs/design/settings-clarity/after/08-connections-advanced-end-120c.png b/docs/design/settings-clarity/after/08-connections-advanced-end-120c.png new file mode 100644 index 0000000000..9da3af478c Binary files /dev/null and b/docs/design/settings-clarity/after/08-connections-advanced-end-120c.png differ diff --git a/docs/design/settings-clarity/after/08-connections-advanced-end-120c.txt b/docs/design/settings-clarity/after/08-connections-advanced-end-120c.txt new file mode 100644 index 0000000000..de432149b2 --- /dev/null +++ b/docs/design/settings-clarity/after/08-connections-advanced-end-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Connections + Models + Memory › Search all settings + Tasks · Notion sign in + AI teams · Todoist sign in + Permissions + Spending sales & outreach + Connections · Amplemarket key + Privacy · Apollo key + · Clari key + · Crunchbase key + · EmailBison key + · HeyReach key + · Hunter key + · Instantly key + · Instantly (Legacy V1) key + · Lemlist key + · Mixmax key + · Outplay key + · Smartlead key + · Super Send key + + support + · ConnectWise key + · Delighted key + · Dixa key + · Freshchat key + · Freshdesk key + · Freshservice key + · Gladly key + · GladlyQA key + · HappyFox key + · Kaseya VSAX key + · Pylon key + · SolarWinds Service Desk key + + Reach your SolarWinds Service Desk account at api.samanage.com, with a key you already hold. + + + + + +─ enter opens a box for the key you already hold ─────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ categories · enter takes your key · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/08-connections-advanced-end-80c.png b/docs/design/settings-clarity/after/08-connections-advanced-end-80c.png new file mode 100644 index 0000000000..a30eca5d87 Binary files /dev/null and b/docs/design/settings-clarity/after/08-connections-advanced-end-80c.png differ diff --git a/docs/design/settings-clarity/after/08-connections-advanced-end-80c.txt b/docs/design/settings-clarity/after/08-connections-advanced-end-80c.txt new file mode 100644 index 0000000000..cd3a98dc7b --- /dev/null +++ b/docs/design/settings-clarity/after/08-connections-advanced-end-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › Search all settings + · Notion sign in + · Todoist sign in + + sales & outreach + · Amplemarket key + · Apollo key + · Clari key + · Crunchbase key + · EmailBison key + · HeyReach key + · Hunter key + · Instantly key + · Instantly (Legacy V1) key + · Lemlist key + · Mixmax key + · Outplay key + · Smartlead key + · Super Send key + + support + · ConnectWise key + · Delighted key + · Dixa key + · Freshchat key + · Freshdesk key + · Freshservice key + · Gladly key + · GladlyQA key + · HappyFox key + · Kaseya VSAX key + · Pylon key + · SolarWinds Service Desk key + + Reach your SolarWinds Service Desk account at api.samanage.com, with a key + you already hold. + + + + +─ enter opens a box for the key you already hold ─────────────────────────────── + ↑↓ move · ←→ categories · enter takes your key · tab next place · esc close diff --git a/docs/design/settings-clarity/after/09-privacy-120c.png b/docs/design/settings-clarity/after/09-privacy-120c.png new file mode 100644 index 0000000000..c4f3053abc Binary files /dev/null and b/docs/design/settings-clarity/after/09-privacy-120c.png differ diff --git a/docs/design/settings-clarity/after/09-privacy-120c.txt b/docs/design/settings-clarity/after/09-privacy-120c.txt new file mode 100644 index 0000000000..33905dca2f --- /dev/null +++ b/docs/design/settings-clarity/after/09-privacy-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Privacy + Models + Memory › Search all settings + Tasks shared model recommendations use and contribute + AI teams save input history on + Permissions share anonymous usage reports on + Spending save unfinished drafts on + Connections Advanced show + Privacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use and contribute receives model recommendations and shares text-free performance measurements. + Use only sends nothing. Off does neither. Code, prompts and file paths are never included. + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/09-privacy-80c.png b/docs/design/settings-clarity/after/09-privacy-80c.png new file mode 100644 index 0000000000..896d5be738 Binary files /dev/null and b/docs/design/settings-clarity/after/09-privacy-80c.png differ diff --git a/docs/design/settings-clarity/after/09-privacy-80c.txt b/docs/design/settings-clarity/after/09-privacy-80c.txt new file mode 100644 index 0000000000..0debc24996 --- /dev/null +++ b/docs/design/settings-clarity/after/09-privacy-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › Search all settings + shared model recommendations use and contribute + save input history on + share anonymous usage reports on + save unfinished drafts on + Advanced show + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use and contribute receives model recommendations and shares text-free + performance measurements. Use only sends nothing. Off does neither. Code, + prompts and file paths are never included. + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-120c.png b/docs/design/settings-clarity/after/09-privacy-advanced-120c.png new file mode 100644 index 0000000000..af14f4c936 Binary files /dev/null and b/docs/design/settings-clarity/after/09-privacy-advanced-120c.png differ diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-120c.txt b/docs/design/settings-clarity/after/09-privacy-advanced-120c.txt new file mode 100644 index 0000000000..5a1cbd82ef --- /dev/null +++ b/docs/design/settings-clarity/after/09-privacy-advanced-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Privacy + Models + Memory › Search all settings + Tasks shared model recommendations use and contribute + AI teams save input history on + Permissions share anonymous usage reports on + Spending save unfinished drafts on + Connections Advanced hide + Privacy model pool signing key built in + + + + + + + + + + + + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-80c.png b/docs/design/settings-clarity/after/09-privacy-advanced-80c.png new file mode 100644 index 0000000000..d2c02cb1c9 Binary files /dev/null and b/docs/design/settings-clarity/after/09-privacy-advanced-80c.png differ diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-80c.txt b/docs/design/settings-clarity/after/09-privacy-advanced-80c.txt new file mode 100644 index 0000000000..09547c1c51 --- /dev/null +++ b/docs/design/settings-clarity/after/09-privacy-advanced-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › Search all settings + shared model recommendations use and contribute + save input history on + share anonymous usage reports on + save unfinished drafts on + Advanced hide + model pool signing key built in + + + + + + + + + + + + + + + + + + + + + + + + + + + + Less common controls. Search finds these even while collapsed. + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter collapse · tab next place · esc close diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-end-120c.png b/docs/design/settings-clarity/after/09-privacy-advanced-end-120c.png new file mode 100644 index 0000000000..130b66f05a Binary files /dev/null and b/docs/design/settings-clarity/after/09-privacy-advanced-end-120c.png differ diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-end-120c.txt b/docs/design/settings-clarity/after/09-privacy-advanced-end-120c.txt new file mode 100644 index 0000000000..614e9d811c --- /dev/null +++ b/docs/design/settings-clarity/after/09-privacy-advanced-end-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Privacy + Models + Memory › Search all settings + Tasks shared model recommendations use and contribute + AI teams save input history on + Permissions share anonymous usage reports on + Spending save unfinished drafts on + Connections Advanced hide + Privacy model pool signing key built in + + + + + + + + + + + + + + + + + + + + + + + + + + + + the public key a Model Pool index must be signed with. Blank trusts the key built into codeaf; + set it to read a relay of your own. + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-end-80c.png b/docs/design/settings-clarity/after/09-privacy-advanced-end-80c.png new file mode 100644 index 0000000000..f1c051d56d Binary files /dev/null and b/docs/design/settings-clarity/after/09-privacy-advanced-end-80c.png differ diff --git a/docs/design/settings-clarity/after/09-privacy-advanced-end-80c.txt b/docs/design/settings-clarity/after/09-privacy-advanced-end-80c.txt new file mode 100644 index 0000000000..f3cfd76d8b --- /dev/null +++ b/docs/design/settings-clarity/after/09-privacy-advanced-end-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › Search all settings + shared model recommendations use and contribute + save input history on + share anonymous usage reports on + save unfinished drafts on + Advanced hide + model pool signing key built in + + + + + + + + + + + + + + + + + + + + + + + + + + + + the public key a Model Pool index must be signed with. Blank trusts the key + built into codeaf; set it to read a relay of your own. + + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · tab next place · esc close diff --git a/docs/design/settings-clarity/after/choice-current-120c.png b/docs/design/settings-clarity/after/choice-current-120c.png new file mode 100644 index 0000000000..9fc4ce4be8 Binary files /dev/null and b/docs/design/settings-clarity/after/choice-current-120c.png differ diff --git a/docs/design/settings-clarity/after/choice-current-120c.txt b/docs/design/settings-clarity/after/choice-current-120c.txt new file mode 100644 index 0000000000..11c5d1924b --- /dev/null +++ b/docs/design/settings-clarity/after/choice-current-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + completed tool details + Collapsed keeps completed tool steps behind one summary. Expanded keeps those details visible. + + collapsed · current + expanded + Cancel esc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ options · enter save · esc cancel diff --git a/docs/design/settings-clarity/after/choice-current-80c.png b/docs/design/settings-clarity/after/choice-current-80c.png new file mode 100644 index 0000000000..36ef8196da Binary files /dev/null and b/docs/design/settings-clarity/after/choice-current-80c.png differ diff --git a/docs/design/settings-clarity/after/choice-current-80c.txt b/docs/design/settings-clarity/after/choice-current-80c.txt new file mode 100644 index 0000000000..6e2f8d76bd --- /dev/null +++ b/docs/design/settings-clarity/after/choice-current-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + completed tool details + Collapsed keeps completed tool steps behind one summary. Expanded keeps + those details visible. + + collapsed · current + expanded + Cancel esc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ options · enter save · esc cancel diff --git a/docs/design/settings-clarity/after/choice-highlighted-120c.png b/docs/design/settings-clarity/after/choice-highlighted-120c.png new file mode 100644 index 0000000000..b1af6e5632 Binary files /dev/null and b/docs/design/settings-clarity/after/choice-highlighted-120c.png differ diff --git a/docs/design/settings-clarity/after/choice-highlighted-120c.txt b/docs/design/settings-clarity/after/choice-highlighted-120c.txt new file mode 100644 index 0000000000..11c5d1924b --- /dev/null +++ b/docs/design/settings-clarity/after/choice-highlighted-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + completed tool details + Collapsed keeps completed tool steps behind one summary. Expanded keeps those details visible. + + collapsed · current + expanded + Cancel esc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ options · enter save · esc cancel diff --git a/docs/design/settings-clarity/after/choice-highlighted-80c.png b/docs/design/settings-clarity/after/choice-highlighted-80c.png new file mode 100644 index 0000000000..3800baadff Binary files /dev/null and b/docs/design/settings-clarity/after/choice-highlighted-80c.png differ diff --git a/docs/design/settings-clarity/after/choice-highlighted-80c.txt b/docs/design/settings-clarity/after/choice-highlighted-80c.txt new file mode 100644 index 0000000000..6e2f8d76bd --- /dev/null +++ b/docs/design/settings-clarity/after/choice-highlighted-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + completed tool details + Collapsed keeps completed tool steps behind one summary. Expanded keeps + those details visible. + + collapsed · current + expanded + Cancel esc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ options · enter save · esc cancel diff --git a/docs/design/settings-clarity/after/choice-reopened-120c.png b/docs/design/settings-clarity/after/choice-reopened-120c.png new file mode 100644 index 0000000000..6217ee7757 Binary files /dev/null and b/docs/design/settings-clarity/after/choice-reopened-120c.png differ diff --git a/docs/design/settings-clarity/after/choice-reopened-120c.txt b/docs/design/settings-clarity/after/choice-reopened-120c.txt new file mode 100644 index 0000000000..1a3dcaee88 --- /dev/null +++ b/docs/design/settings-clarity/after/choice-reopened-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + completed tool details + Collapsed keeps completed tool steps behind one summary. Expanded keeps those details visible. + + collapsed + expanded · current + Cancel esc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ options · enter save · esc cancel diff --git a/docs/design/settings-clarity/after/choice-reopened-80c.png b/docs/design/settings-clarity/after/choice-reopened-80c.png new file mode 100644 index 0000000000..b74e4864c7 Binary files /dev/null and b/docs/design/settings-clarity/after/choice-reopened-80c.png differ diff --git a/docs/design/settings-clarity/after/choice-reopened-80c.txt b/docs/design/settings-clarity/after/choice-reopened-80c.txt new file mode 100644 index 0000000000..8671073fe4 --- /dev/null +++ b/docs/design/settings-clarity/after/choice-reopened-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + completed tool details + Collapsed keeps completed tool steps behind one summary. Expanded keeps + those details visible. + + collapsed + expanded · current + Cancel esc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + ↑↓ options · enter save · esc cancel diff --git a/docs/design/settings-clarity/after/choice-saved-120c.png b/docs/design/settings-clarity/after/choice-saved-120c.png new file mode 100644 index 0000000000..cf47713d00 Binary files /dev/null and b/docs/design/settings-clarity/after/choice-saved-120c.png differ diff --git a/docs/design/settings-clarity/after/choice-saved-120c.txt b/docs/design/settings-clarity/after/choice-saved-120c.txt new file mode 100644 index 0000000000..3baa113fee --- /dev/null +++ b/docs/design/settings-clarity/after/choice-saved-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › ui.work + Tasks General + AI teams completed tool details • expanded + Permissions automatic updates on + Spending + Connections Spending + Privacy daily spending limit $500 + + Models + reasoning effort auto + + + + + + + + + + + + + + + + + + + + + + + + + Collapsed keeps completed tool steps behind one summary. Expanded keeps those details visible. + + + + + +─ Saved ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/choice-saved-80c.png b/docs/design/settings-clarity/after/choice-saved-80c.png new file mode 100644 index 0000000000..7f312c8b87 Binary files /dev/null and b/docs/design/settings-clarity/after/choice-saved-80c.png differ diff --git a/docs/design/settings-clarity/after/choice-saved-80c.txt b/docs/design/settings-clarity/after/choice-saved-80c.txt new file mode 100644 index 0000000000..e3a83afaff --- /dev/null +++ b/docs/design/settings-clarity/after/choice-saved-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › ui.work + General + completed tool details • expanded + automatic updates on + + Spending + daily spending limit $500 + + Models + reasoning effort auto + + + + + + + + + + + + + + + + + + + + + + + + + Collapsed keeps completed tool steps behind one summary. Expanded keeps + those details visible. + + + + +─ Saved ──────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-hints-after-120c.png b/docs/design/settings-clarity/after/edit-hints-after-120c.png new file mode 100644 index 0000000000..c1021ff415 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-hints-after-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-hints-after-120c.txt b/docs/design/settings-clarity/after/edit-hints-after-120c.txt new file mode 100644 index 0000000000..46dd3cf6f0 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-hints-after-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › ui.hints + Tasks General + AI teams show hints • off + Permissions automatic updates on + Spending background reminders off + Connections + Privacy Privacy + model pool signing key built in + share anonymous usage reports on + shared model recommendations use and contribute + + Memory + remember across chats on + + Spending + daily spending limit $500 + + Models + chat model deepseek/deepseek-v4.1-flash + reasoning effort auto + + Permissions + tool approvals allow by default + + Tasks + background shell commands after 30s + concurrent tasks no limit + + + + + + + + Show contextual tips while you work. + + + + + +─ Saved ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-hints-after-80c.png b/docs/design/settings-clarity/after/edit-hints-after-80c.png new file mode 100644 index 0000000000..7f2440a73b Binary files /dev/null and b/docs/design/settings-clarity/after/edit-hints-after-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-hints-after-80c.txt b/docs/design/settings-clarity/after/edit-hints-after-80c.txt new file mode 100644 index 0000000000..b3ef419bf3 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-hints-after-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › ui.hints + General + show hints • off + automatic updates on + background reminders off + + Privacy + model pool signing key built in + share anonymous usage reports on + shared model recommendations use and contribute + + Memory + remember across chats on + + Spending + daily spending limit $500 + + Models + chat model deepseek/deepseek-v4.1-flash + reasoning effort auto + + Permissions + tool approvals allow by default + + Tasks + background shell commands after 30s + concurrent tasks no limit + + + + + + + + Show contextual tips while you work. + + + + + +─ Saved ──────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-hints-before-120c.png b/docs/design/settings-clarity/after/edit-hints-before-120c.png new file mode 100644 index 0000000000..d1890163d1 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-hints-before-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-hints-before-120c.txt b/docs/design/settings-clarity/after/edit-hints-before-120c.txt new file mode 100644 index 0000000000..b40ba25036 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-hints-before-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › ui.hints + Tasks General + AI teams show hints on + Permissions automatic updates on + Spending background reminders off + Connections + Privacy Privacy + model pool signing key built in + share anonymous usage reports on + shared model recommendations use and contribute + + Memory + remember across chats on + + Spending + daily spending limit $500 + + Models + chat model deepseek/deepseek-v4.1-flash + reasoning effort auto + + Permissions + tool approvals allow by default + + Tasks + background shell commands after 30s + concurrent tasks no limit + + + + + + + + Show contextual tips while you work. + + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-hints-before-80c.png b/docs/design/settings-clarity/after/edit-hints-before-80c.png new file mode 100644 index 0000000000..97318428f7 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-hints-before-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-hints-before-80c.txt b/docs/design/settings-clarity/after/edit-hints-before-80c.txt new file mode 100644 index 0000000000..612dba0357 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-hints-before-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › ui.hints + General + show hints on + automatic updates on + background reminders off + + Privacy + model pool signing key built in + share anonymous usage reports on + shared model recommendations use and contribute + + Memory + remember across chats on + + Spending + daily spending limit $500 + + Models + chat model deepseek/deepseek-v4.1-flash + reasoning effort auto + + Permissions + tool approvals allow by default + + Tasks + background shell commands after 30s + concurrent tasks no limit + + + + + + + + Show contextual tips while you work. + + + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-invalid-value-120c.png b/docs/design/settings-clarity/after/edit-invalid-value-120c.png new file mode 100644 index 0000000000..31d528545e Binary files /dev/null and b/docs/design/settings-clarity/after/edit-invalid-value-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-invalid-value-120c.txt b/docs/design/settings-clarity/after/edit-invalid-value-120c.txt new file mode 100644 index 0000000000..5de79ac014 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-invalid-value-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + concurrent tasks + a whole number · blank for no limit + › invalid + that's not a whole number + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-invalid-value-80c.png b/docs/design/settings-clarity/after/edit-invalid-value-80c.png new file mode 100644 index 0000000000..e07d727650 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-invalid-value-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-invalid-value-80c.txt b/docs/design/settings-clarity/after/edit-invalid-value-80c.txt new file mode 100644 index 0000000000..6b26bb9baf --- /dev/null +++ b/docs/design/settings-clarity/after/edit-invalid-value-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + concurrent tasks + a whole number · blank for no limit + › invalid + that's not a whole number + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-limit-cleared-120c.png b/docs/design/settings-clarity/after/edit-limit-cleared-120c.png new file mode 100644 index 0000000000..0364d4a6f4 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-cleared-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-cleared-120c.txt b/docs/design/settings-clarity/after/edit-limit-cleared-120c.txt new file mode 100644 index 0000000000..0cbb83faec --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-cleared-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › task.parallel + Tasks Tasks + AI teams concurrent tasks no limit + Permissions task scope assessment assess while working + Spending + Connections Permissions + Privacy tool approvals allow by default + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are + the real ceilings. + + + +─ Saved ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-limit-cleared-80c.png b/docs/design/settings-clarity/after/edit-limit-cleared-80c.png new file mode 100644 index 0000000000..3b816b03bf Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-cleared-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-cleared-80c.txt b/docs/design/settings-clarity/after/edit-limit-cleared-80c.txt new file mode 100644 index 0000000000..019359ebd3 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-cleared-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › task.parallel + Tasks + concurrent tasks no limit + task scope assessment assess while working + + Permissions + tool approvals allow by default + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + + + +─ Saved ──────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-limit-reopened-120c.png b/docs/design/settings-clarity/after/edit-limit-reopened-120c.png new file mode 100644 index 0000000000..3176e4d9a6 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-reopened-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-reopened-120c.txt b/docs/design/settings-clarity/after/edit-limit-reopened-120c.txt new file mode 100644 index 0000000000..40a220c83e --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-reopened-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + concurrent tasks + a whole number · blank for no limit + › 4 + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-limit-reopened-80c.png b/docs/design/settings-clarity/after/edit-limit-reopened-80c.png new file mode 100644 index 0000000000..e89be5a4ca Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-reopened-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-reopened-80c.txt b/docs/design/settings-clarity/after/edit-limit-reopened-80c.txt new file mode 100644 index 0000000000..f2d953203a --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-reopened-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + concurrent tasks + a whole number · blank for no limit + › 4 + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-limit-saved-120c.png b/docs/design/settings-clarity/after/edit-limit-saved-120c.png new file mode 100644 index 0000000000..d7afb2f9eb Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-saved-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-saved-120c.txt b/docs/design/settings-clarity/after/edit-limit-saved-120c.txt new file mode 100644 index 0000000000..7ab41a84f6 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-saved-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › task.parallel + Tasks Tasks + AI teams concurrent tasks • 4 + Permissions task scope assessment assess while working + Spending + Connections Permissions + Privacy tool approvals allow by default + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are + the real ceilings. + + + +─ Saved ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-limit-saved-80c.png b/docs/design/settings-clarity/after/edit-limit-saved-80c.png new file mode 100644 index 0000000000..bb5cc5fa33 Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-saved-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-saved-80c.txt b/docs/design/settings-clarity/after/edit-limit-saved-80c.txt new file mode 100644 index 0000000000..e5133a834e --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-saved-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › task.parallel + Tasks + concurrent tasks • 4 + task scope assessment assess while working + + Permissions + tool approvals allow by default + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + + + +─ Saved ──────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-limit-unlimited-120c.png b/docs/design/settings-clarity/after/edit-limit-unlimited-120c.png new file mode 100644 index 0000000000..b6bc9fcbbd Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-unlimited-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-unlimited-120c.txt b/docs/design/settings-clarity/after/edit-limit-unlimited-120c.txt new file mode 100644 index 0000000000..a47086aaee --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-unlimited-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + concurrent tasks + a whole number · blank for no limit + › Enter value + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-limit-unlimited-80c.png b/docs/design/settings-clarity/after/edit-limit-unlimited-80c.png new file mode 100644 index 0000000000..839796fe2b Binary files /dev/null and b/docs/design/settings-clarity/after/edit-limit-unlimited-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-limit-unlimited-80c.txt b/docs/design/settings-clarity/after/edit-limit-unlimited-80c.txt new file mode 100644 index 0000000000..e2b4790b4e --- /dev/null +++ b/docs/design/settings-clarity/after/edit-limit-unlimited-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + concurrent tasks + a whole number · blank for no limit + › Enter value + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-number-cancelled-120c.png b/docs/design/settings-clarity/after/edit-number-cancelled-120c.png new file mode 100644 index 0000000000..ab6a430a8b Binary files /dev/null and b/docs/design/settings-clarity/after/edit-number-cancelled-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-number-cancelled-120c.txt b/docs/design/settings-clarity/after/edit-number-cancelled-120c.txt new file mode 100644 index 0000000000..4ea0064b27 --- /dev/null +++ b/docs/design/settings-clarity/after/edit-number-cancelled-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › task.parallel + Tasks Tasks + AI teams concurrent tasks no limit + Permissions task scope assessment assess while working + Spending + Connections Permissions + Privacy tool approvals allow by default + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are + the real ceilings. + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-number-cancelled-80c.png b/docs/design/settings-clarity/after/edit-number-cancelled-80c.png new file mode 100644 index 0000000000..64cf5e413e Binary files /dev/null and b/docs/design/settings-clarity/after/edit-number-cancelled-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-number-cancelled-80c.txt b/docs/design/settings-clarity/after/edit-number-cancelled-80c.txt new file mode 100644 index 0000000000..408f77eb4b --- /dev/null +++ b/docs/design/settings-clarity/after/edit-number-cancelled-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › task.parallel + Tasks + concurrent tasks no limit + task scope assessment assess while working + + Permissions + tool approvals allow by default + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/edit-number-open-120c.png b/docs/design/settings-clarity/after/edit-number-open-120c.png new file mode 100644 index 0000000000..b6bc9fcbbd Binary files /dev/null and b/docs/design/settings-clarity/after/edit-number-open-120c.png differ diff --git a/docs/design/settings-clarity/after/edit-number-open-120c.txt b/docs/design/settings-clarity/after/edit-number-open-120c.txt new file mode 100644 index 0000000000..a47086aaee --- /dev/null +++ b/docs/design/settings-clarity/after/edit-number-open-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending Connections Privacy + + concurrent tasks + a whole number · blank for no limit + › Enter value + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/edit-number-open-80c.png b/docs/design/settings-clarity/after/edit-number-open-80c.png new file mode 100644 index 0000000000..839796fe2b Binary files /dev/null and b/docs/design/settings-clarity/after/edit-number-open-80c.png differ diff --git a/docs/design/settings-clarity/after/edit-number-open-80c.txt b/docs/design/settings-clarity/after/edit-number-open-80c.txt new file mode 100644 index 0000000000..e2b4790b4e --- /dev/null +++ b/docs/design/settings-clarity/after/edit-number-open-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + concurrent tasks + a whole number · blank for no limit + › Enter value + Save enter Cancel esc + + Restart the CLI to apply this to already-open chats + how many tasks may run at the same time. Blank is no limit — the machine and + the provider are the real ceilings. + This profile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + enter save · esc cancel diff --git a/docs/design/settings-clarity/after/hover-category-120c.png b/docs/design/settings-clarity/after/hover-category-120c.png new file mode 100644 index 0000000000..ae534db927 Binary files /dev/null and b/docs/design/settings-clarity/after/hover-category-120c.png differ diff --git a/docs/design/settings-clarity/after/hover-category-120c.txt b/docs/design/settings-clarity/after/hover-category-120c.txt new file mode 100644 index 0000000000..e0941158d1 --- /dev/null +++ b/docs/design/settings-clarity/after/hover-category-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General General + Models + Memory › Search all settings + Tasks mouse interaction on + AI teams timestamps footers + Permissions completed tool details • expanded + Spending tool icons auto + Connections automatic updates on + Privacy background reminders off + task sidebar on + switch chats immediately on + show hints • off + include model in commit attribution on + + + + + + + + + + + + + + + + + + + + + + + + on gives hover and click; off gives the terminal's own text selection back. + + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ settings · ←→ categories · enter change · type to search all · tab next place · esc close diff --git a/docs/design/settings-clarity/after/navigation-activity-120c.png b/docs/design/settings-clarity/after/navigation-activity-120c.png new file mode 100644 index 0000000000..aa699d1de6 Binary files /dev/null and b/docs/design/settings-clarity/after/navigation-activity-120c.png differ diff --git a/docs/design/settings-clarity/after/navigation-activity-120c.txt b/docs/design/settings-clarity/after/navigation-activity-120c.txt new file mode 100644 index 0000000000..0de1616b20 --- /dev/null +++ b/docs/design/settings-clarity/after/navigation-activity-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + 1 chat shift+← sep 24 – oct 7 → shift+↑ coarser +  type to filter project age ↓ │ new conversation + │ + completed │ enter open the chat + · new conversation project │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + enter go to that conversation · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/after/navigation-activity-80c.png b/docs/design/settings-clarity/after/navigation-activity-80c.png new file mode 100644 index 0000000000..6312b6002a Binary files /dev/null and b/docs/design/settings-clarity/after/navigation-activity-80c.png differ diff --git a/docs/design/settings-clarity/after/navigation-activity-80c.txt b/docs/design/settings-clarity/after/navigation-activity-80c.txt new file mode 100644 index 0000000000..eec7f1b902 --- /dev/null +++ b/docs/design/settings-clarity/after/navigation-activity-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + 1 chat shift+← sep 24 – oct 7 → shift+↑ coarser +  type to filter project age ↓ + + completed + · new conversation project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + enter go to that conversation · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/after/navigation-ai-teams-120c.png b/docs/design/settings-clarity/after/navigation-ai-teams-120c.png new file mode 100644 index 0000000000..eabe11c40b Binary files /dev/null and b/docs/design/settings-clarity/after/navigation-ai-teams-120c.png differ diff --git a/docs/design/settings-clarity/after/navigation-ai-teams-120c.txt b/docs/design/settings-clarity/after/navigation-ai-teams-120c.txt new file mode 100644 index 0000000000..a72d75a4bb --- /dev/null +++ b/docs/design/settings-clarity/after/navigation-ai-teams-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +  All teams │╭─ Global manager ────────────────────────────────────────────────────────────────────────────╮ + + New AI team ││ Optional · coordinates the managers of top-level teams │ + ││ + Global manager │ + │╰─────────────────────────────────────────────────────────────────────────────────────────────╯ + │ + │ teams ✦ Organize + │ + │ + │ An AI team groups chats around ongoing work. Add an optional AI manager + │ to coordinate them. + │ + │ ✦ Organize my conversations + New AI team + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Every team, and what waits on you from any of them · enter · tab next place diff --git a/docs/design/settings-clarity/after/navigation-ai-teams-80c.png b/docs/design/settings-clarity/after/navigation-ai-teams-80c.png new file mode 100644 index 0000000000..7a17a19458 Binary files /dev/null and b/docs/design/settings-clarity/after/navigation-ai-teams-80c.png differ diff --git a/docs/design/settings-clarity/after/navigation-ai-teams-80c.txt b/docs/design/settings-clarity/after/navigation-ai-teams-80c.txt new file mode 100644 index 0000000000..f5aa8df48d --- /dev/null +++ b/docs/design/settings-clarity/after/navigation-ai-teams-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + +  All teams │╭─ Global manager ──────────────────────────────────────╮ + + New AI team ││ Optional · coordinates the managers of top-level │ + ││ teams │ + ││ + Global manager │ + │╰───────────────────────────────────────────────────────╯ + │ + │ teams ✦ Organize + │ + │ + │ An AI team groups chats around ongoing work. Add an + │ optional AI manager to coordinate them. + │ + │ ✦ Organize my conversations + New AI team + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +──────────────────────────────────────────────────────────────────────────────── + Every team, and what waits on you from any of them · enter · tab next place diff --git a/docs/design/settings-clarity/after/search-old-label-120c.png b/docs/design/settings-clarity/after/search-old-label-120c.png new file mode 100644 index 0000000000..734488f50c Binary files /dev/null and b/docs/design/settings-clarity/after/search-old-label-120c.png differ diff --git a/docs/design/settings-clarity/after/search-old-label-120c.txt b/docs/design/settings-clarity/after/search-old-label-120c.txt new file mode 100644 index 0000000000..39a18b844f --- /dev/null +++ b/docs/design/settings-clarity/after/search-old-label-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › memory floor + Tasks Tasks + AI teams minimum available RAM 1536 MB + Permissions task scope assessment assess while working + Spending + Connections Memory + Privacy remember across chats on + + General + automatic updates on + + Models + prompt profile auto + reasoning effort auto + compact context at 60% + model provider auto + routing simple + + Connections + openrouter key • ••••••••used + + Spending + daily spending limit $500 + + Privacy + share anonymous usage reports on + model pool signing key built in + shared model recommendations use and contribute + + AI teams + ask the AI manager first on + + + + MB of memory that must be free before another task starts. 0 stops watching. + + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/search-old-label-80c.png b/docs/design/settings-clarity/after/search-old-label-80c.png new file mode 100644 index 0000000000..45531efcaa Binary files /dev/null and b/docs/design/settings-clarity/after/search-old-label-80c.png differ diff --git a/docs/design/settings-clarity/after/search-old-label-80c.txt b/docs/design/settings-clarity/after/search-old-label-80c.txt new file mode 100644 index 0000000000..9223eeb25a --- /dev/null +++ b/docs/design/settings-clarity/after/search-old-label-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › memory floor + Tasks + minimum available RAM 1536 MB + task scope assessment assess while working + + Memory + remember across chats on + + General + automatic updates on + + Models + prompt profile auto + reasoning effort auto + compact context at 60% + model provider auto + routing simple + + Connections + openrouter key • ••••••••used + + Spending + daily spending limit $500 + + Privacy + share anonymous usage reports on + model pool signing key built in + shared model recommendations use and contribute + + AI teams + ask the AI manager first on + + + + MB of memory that must be free before another task starts. 0 stops watching. + + + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/search-privacy-120c.png b/docs/design/settings-clarity/after/search-privacy-120c.png new file mode 100644 index 0000000000..8dfaf4097c Binary files /dev/null and b/docs/design/settings-clarity/after/search-privacy-120c.png differ diff --git a/docs/design/settings-clarity/after/search-privacy-120c.txt b/docs/design/settings-clarity/after/search-privacy-120c.txt new file mode 100644 index 0000000000..c30d76246d --- /dev/null +++ b/docs/design/settings-clarity/after/search-privacy-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › privacy + Tasks Privacy + AI teams shared model recommendations use and contribute + Permissions model pool signing key built in + Spending save input history on + Connections share anonymous usage reports on + Privacy save unfinished drafts on + + Memory + remember across chats on + + Models + routing simple + + Tasks + task scope assessment assess while working + + + + + + + + + + + + + + + + + + + Use and contribute receives model recommendations and shares text-free performance measurements. + Use only sends nothing. Off does neither. Code, prompts and file paths are never included. + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/search-privacy-80c.png b/docs/design/settings-clarity/after/search-privacy-80c.png new file mode 100644 index 0000000000..054ee9b124 Binary files /dev/null and b/docs/design/settings-clarity/after/search-privacy-80c.png differ diff --git a/docs/design/settings-clarity/after/search-privacy-80c.txt b/docs/design/settings-clarity/after/search-privacy-80c.txt new file mode 100644 index 0000000000..0143b10313 --- /dev/null +++ b/docs/design/settings-clarity/after/search-privacy-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + … Memory Tasks AI teams Permissions Spending Connections Privacy + + › privacy + Privacy + shared model recommendations use and contribute + model pool signing key built in + save input history on + share anonymous usage reports on + save unfinished drafts on + + Memory + remember across chats on + + Models + routing simple + + Tasks + task scope assessment assess while working + + + + + + + + + + + + + + + + + + + Use and contribute receives model recommendations and shares text-free + performance measurements. Use only sends nothing. Off does neither. Code, + prompts and file paths are never included. + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/search-service-120c.png b/docs/design/settings-clarity/after/search-service-120c.png new file mode 100644 index 0000000000..ffbad80021 Binary files /dev/null and b/docs/design/settings-clarity/after/search-service-120c.png differ diff --git a/docs/design/settings-clarity/after/search-service-120c.txt b/docs/design/settings-clarity/after/search-service-120c.txt new file mode 100644 index 0000000000..b46459ef20 --- /dev/null +++ b/docs/design/settings-clarity/after/search-service-120c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings wed 4:27pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + General Search results + Models + Memory › github + Tasks General + AI teams background reminders off + Permissions automatic updates on + Spending + Connections Models + Privacy reasoning effort auto + model provider auto + fallback models nearest in the catalog + routing simple + + Privacy + model pool signing key built in + + AI teams + maximum team nesting 3 levels + + + + + + + + + + + + + + + + + + + Check reminders, watches and routines while the CLI is closed. Uses a timer under your login; + the machine must be awake and you must be logged in. + + + + +─ This profile ───────────────────────────────────────────────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/after/search-service-80c.png b/docs/design/settings-clarity/after/search-service-80c.png new file mode 100644 index 0000000000..670b2b52c9 Binary files /dev/null and b/docs/design/settings-clarity/after/search-service-80c.png differ diff --git a/docs/design/settings-clarity/after/search-service-80c.txt b/docs/design/settings-clarity/after/search-service-80c.txt new file mode 100644 index 0000000000..f5c7afd6f0 --- /dev/null +++ b/docs/design/settings-clarity/after/search-service-80c.txt @@ -0,0 +1,48 @@ + >● codeaf Home Chats AI teams Activity Memory Spend Settings + +──────────────────────────────────────────────────────────────────────────────── + + General Models Memory Tasks AI teams Permissions Spending … + + › github + General + background reminders off + automatic updates on + + Models + reasoning effort auto + model provider auto + fallback models nearest in the catalog + routing simple + + Privacy + model pool signing key built in + + AI teams + maximum team nesting 3 levels + + + + + + + + + + + + + + + + + + + Check reminders, watches and routines while the CLI is closed. Uses a timer + under your login; the machine must be awake and you must be logged in. + + + + +─ This profile ───────────────────────────────────────────────────────────────── + ↑↓ results · enter change · tab next place · esc clear search diff --git a/docs/design/settings-clarity/before/01-session-120c.png b/docs/design/settings-clarity/before/01-session-120c.png new file mode 100644 index 0000000000..8842cc201e Binary files /dev/null and b/docs/design/settings-clarity/before/01-session-120c.png differ diff --git a/docs/design/settings-clarity/before/01-session-120c.txt b/docs/design/settings-clarity/before/01-session-120c.txt new file mode 100644 index 0000000000..865b7e413b --- /dev/null +++ b/docs/design/settings-clarity/before/01-session-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + memory on + a few things are carried from one conversation to the next. Off, each one starts knowing nothing about you. + fallback models nearest in the catalog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/01-session-80c.png b/docs/design/settings-clarity/before/01-session-80c.png new file mode 100644 index 0000000000..605d315aac Binary files /dev/null and b/docs/design/settings-clarity/before/01-session-80c.png differ diff --git a/docs/design/settings-clarity/before/01-session-80c.txt b/docs/design/settings-clarity/before/01-session-80c.txt new file mode 100644 index 0000000000..87d9628a2f --- /dev/null +++ b/docs/design/settings-clarity/before/01-session-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + memory on + a few things are carried from one conversation to the next. Off, each one + starts knowing nothing about you. + fallback models nearest in the catalog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/02-context-120c.png b/docs/design/settings-clarity/before/02-context-120c.png new file mode 100644 index 0000000000..017c0b80ed Binary files /dev/null and b/docs/design/settings-clarity/before/02-context-120c.png differ diff --git a/docs/design/settings-clarity/before/02-context-120c.txt b/docs/design/settings-clarity/before/02-context-120c.txt new file mode 100644 index 0000000000..37629eb810 --- /dev/null +++ b/docs/design/settings-clarity/before/02-context-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + searching auto + now firecrawl, keyless — set search.exaKey or search.firecrawlKey to raise it + exa key not set + firecrawl key not set + jina key not set + compact at 60% + answer room 65536 tok + working set 160000 tok + context reuse 250% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/02-context-80c.png b/docs/design/settings-clarity/before/02-context-80c.png new file mode 100644 index 0000000000..d552301e90 Binary files /dev/null and b/docs/design/settings-clarity/before/02-context-80c.png differ diff --git a/docs/design/settings-clarity/before/02-context-80c.txt b/docs/design/settings-clarity/before/02-context-80c.txt new file mode 100644 index 0000000000..0ea563cfba --- /dev/null +++ b/docs/design/settings-clarity/before/02-context-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + searching auto + now firecrawl, keyless — set search.exaKey or search.firecrawlKey to raise + it + exa key not set + firecrawl key not set + jina key not set + compact at 60% + answer room 65536 tok + working set 160000 tok + context reuse 250% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/03-workspace-120c.png b/docs/design/settings-clarity/before/03-workspace-120c.png new file mode 100644 index 0000000000..61f4471248 Binary files /dev/null and b/docs/design/settings-clarity/before/03-workspace-120c.png differ diff --git a/docs/design/settings-clarity/before/03-workspace-120c.txt b/docs/design/settings-clarity/before/03-workspace-120c.txt new file mode 100644 index 0000000000..a0daad0b03 --- /dev/null +++ b/docs/design/settings-clarity/before/03-workspace-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + google sign-in id not set + identifies codeaf to Google when you connect an account. Blank uses the one codeaf ships with. + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings + auto update on + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/03-workspace-80c.png b/docs/design/settings-clarity/before/03-workspace-80c.png new file mode 100644 index 0000000000..df310c2e53 Binary files /dev/null and b/docs/design/settings-clarity/before/03-workspace-80c.png differ diff --git a/docs/design/settings-clarity/before/03-workspace-80c.txt b/docs/design/settings-clarity/before/03-workspace-80c.txt new file mode 100644 index 0000000000..e392dc6710 --- /dev/null +++ b/docs/design/settings-clarity/before/03-workspace-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + google sign-in id not set + identifies codeaf to Google when you connect an account. Blank uses the + one codeaf ships with. + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings + auto update on + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/04-display-120c.png b/docs/design/settings-clarity/before/04-display-120c.png new file mode 100644 index 0000000000..08eea0234d Binary files /dev/null and b/docs/design/settings-clarity/before/04-display-120c.png differ diff --git a/docs/design/settings-clarity/before/04-display-120c.txt b/docs/design/settings-clarity/before/04-display-120c.txt new file mode 100644 index 0000000000..9a903a23dc --- /dev/null +++ b/docs/design/settings-clarity/before/04-display-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + mouse on + on gives hover and click; off gives the terminal's own text selection back. + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/04-display-80c.png b/docs/design/settings-clarity/before/04-display-80c.png new file mode 100644 index 0000000000..9e6c4278c3 Binary files /dev/null and b/docs/design/settings-clarity/before/04-display-80c.png differ diff --git a/docs/design/settings-clarity/before/04-display-80c.txt b/docs/design/settings-clarity/before/04-display-80c.txt new file mode 100644 index 0000000000..412fc14c36 --- /dev/null +++ b/docs/design/settings-clarity/before/04-display-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + mouse on + on gives hover and click; off gives the terminal's own text selection + back. + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/05-spending-120c.png b/docs/design/settings-clarity/before/05-spending-120c.png new file mode 100644 index 0000000000..77f3505469 Binary files /dev/null and b/docs/design/settings-clarity/before/05-spending-120c.png differ diff --git a/docs/design/settings-clarity/before/05-spending-120c.txt b/docs/design/settings-clarity/before/05-spending-120c.txt new file mode 100644 index 0000000000..9642dfc0a5 --- /dev/null +++ b/docs/design/settings-clarity/before/05-spending-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + per day $500 + what codeaf may spend on your work in a day. When the day's calls reach it, new work waits for midnight or for you + to raise it here. none removes the limit. + per conversation no limit + per plan asks first above $100 + per task $5 a task · set in /crew · it also spends against the day and this conversation + per standing run $5 a firing · each order may name its own + practice $50 of the day + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/05-spending-80c.png b/docs/design/settings-clarity/before/05-spending-80c.png new file mode 100644 index 0000000000..1b693e2171 Binary files /dev/null and b/docs/design/settings-clarity/before/05-spending-80c.png differ diff --git a/docs/design/settings-clarity/before/05-spending-80c.txt b/docs/design/settings-clarity/before/05-spending-80c.txt new file mode 100644 index 0000000000..bcfd361769 --- /dev/null +++ b/docs/design/settings-clarity/before/05-spending-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + per day $500 + what codeaf may spend on your work in a day. When the day's calls reach + it, new work waits for midnight or for you to raise it here. none removes + the limit. + per conversation no limit + per plan asks first above $100 + per task $5 a task · set in /crew + per standing run $5 a firing · each order may name its own + practice $50 of the day + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/06-safety-120c.png b/docs/design/settings-clarity/before/06-safety-120c.png new file mode 100644 index 0000000000..a1b7d9fea0 Binary files /dev/null and b/docs/design/settings-clarity/before/06-safety-120c.png differ diff --git a/docs/design/settings-clarity/before/06-safety-120c.txt b/docs/design/settings-clarity/before/06-safety-120c.txt new file mode 100644 index 0000000000..f8f5126816 --- /dev/null +++ b/docs/design/settings-clarity/before/06-safety-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + ask before running allow + what happens when the model asks to run a tool. Dangerous shell commands are asked about whichever way this is + set. + tool exceptions none + shell command rules none + guardian off + approval countdown 10s + + questions while you are away + permission ask me + choice ask me + judgement ask me + clarification ask me · never runs on a clock + confirmation ask me · destructive always asks + landing ask me + assumption ask me + ratify ask me + background after 30s + who settles work that needs a look ask + task countdown 15s + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/06-safety-80c.png b/docs/design/settings-clarity/before/06-safety-80c.png new file mode 100644 index 0000000000..49d1f5fc56 Binary files /dev/null and b/docs/design/settings-clarity/before/06-safety-80c.png differ diff --git a/docs/design/settings-clarity/before/06-safety-80c.txt b/docs/design/settings-clarity/before/06-safety-80c.txt new file mode 100644 index 0000000000..fe1fd85a78 --- /dev/null +++ b/docs/design/settings-clarity/before/06-safety-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + ask before running allow + what happens when the model asks to run a tool. Dangerous shell commands + are asked about whichever way this is set. + tool exceptions none + shell command rules none + guardian off + approval countdown 10s + + questions while you are away + permission ask me + choice ask me + judgement ask me + clarification ask me · never runs on a clock + confirmation ask me · destructive always asks + landing ask me + assumption ask me + ratify ask me + background after 30s + who settles work that needs a look ask + task countdown 15s + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/07-tasks-120c.png b/docs/design/settings-clarity/before/07-tasks-120c.png new file mode 100644 index 0000000000..029822fdd6 Binary files /dev/null and b/docs/design/settings-clarity/before/07-tasks-120c.png differ diff --git a/docs/design/settings-clarity/before/07-tasks-120c.txt b/docs/design/settings-clarity/before/07-tasks-120c.txt new file mode 100644 index 0000000000..6f2f22d411 --- /dev/null +++ b/docs/design/settings-clarity/before/07-tasks-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + starting a task sized + what /task does with your brief: sized reads it for width first, so the one worker that starts can hand the parts + out once it has opened the material, single starts that worker without reading the brief at all. + check task work on + task repair rounds 1 + tasks at once no limit + busy machine 1.5 per core + memory floor 1536 MB + task model the crew's worker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/07-tasks-80c.png b/docs/design/settings-clarity/before/07-tasks-80c.png new file mode 100644 index 0000000000..af42c80075 Binary files /dev/null and b/docs/design/settings-clarity/before/07-tasks-80c.png differ diff --git a/docs/design/settings-clarity/before/07-tasks-80c.txt b/docs/design/settings-clarity/before/07-tasks-80c.txt new file mode 100644 index 0000000000..17dff1c713 --- /dev/null +++ b/docs/design/settings-clarity/before/07-tasks-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + starting a task sized + what /task does with your brief: sized reads it for width first, so the + one worker that starts can hand the parts out once it has opened the + material, single starts that worker without reading the brief at all. + check task work on + task repair rounds 1 + tasks at once no limit + busy machine 1.5 per core + memory floor 1536 MB + task model the crew's worker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/08-teams-120c.png b/docs/design/settings-clarity/before/08-teams-120c.png new file mode 100644 index 0000000000..9b32686c01 Binary files /dev/null and b/docs/design/settings-clarity/before/08-teams-120c.png differ diff --git a/docs/design/settings-clarity/before/08-teams-120c.txt b/docs/design/settings-clarity/before/08-teams-120c.txt new file mode 100644 index 0000000000..ac86dcd46a --- /dev/null +++ b/docs/design/settings-clarity/before/08-teams-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + questions go to the manager on + a member's clarifying question goes to its manager first; you are asked only what no manager can answer. + Permission prompts always come to you. + team messages wake on + daily cap per team no cap + team depth 3 levels + sub-team share 50% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ a team can override any of these on its card · saved to your profile ───────────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/08-teams-80c.png b/docs/design/settings-clarity/before/08-teams-80c.png new file mode 100644 index 0000000000..a1639f4bd0 Binary files /dev/null and b/docs/design/settings-clarity/before/08-teams-80c.png differ diff --git a/docs/design/settings-clarity/before/08-teams-80c.txt b/docs/design/settings-clarity/before/08-teams-80c.txt new file mode 100644 index 0000000000..693d2ac059 --- /dev/null +++ b/docs/design/settings-clarity/before/08-teams-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + questions go to the manager on + a member's clarifying question goes to its manager first; you are asked + only what no manager can answer. Permission prompts always come to you. + team messages wake on + daily cap per team no cap + team depth 3 levels + sub-team share 50% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ a team can override any of these on its card · saved to your profile ───────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/09-providers-120c.png b/docs/design/settings-clarity/before/09-providers-120c.png new file mode 100644 index 0000000000..2d44270a97 Binary files /dev/null and b/docs/design/settings-clarity/before/09-providers-120c.png differ diff --git a/docs/design/settings-clarity/before/09-providers-120c.txt b/docs/design/settings-clarity/before/09-providers-120c.txt new file mode 100644 index 0000000000..90aef01c2b --- /dev/null +++ b/docs/design/settings-clarity/before/09-providers-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + your model deepseek/deepseek-v4.1-flash + the model you are talking to. Everything below it is a model codeaf uses on your behalf. + + routing + host auto + speed guard on + routing simple + prompt profile auto + reflex • deepseek/deepseek-v4.1-flash + small work • deepseek/deepseek-v4.1-flash + seats auto · 3 pinned · models all · 1 of 1 providers · per task $5 + pinned roles none + + roles · reflex + reflex deepseek/deepseek-v4.1-flash + + roles · small work + caption deepseek/deepseek-v4.1-flash + consolidate deepseek/deepseek-v4.1-flash + guardian deepseek/deepseek-v4.1-flash + intake deepseek/deepseek-v4.1-flash + jobname deepseek/deepseek-v4.1-flash + router deepseek/deepseek-v4.1-flash + sentinel deepseek/deepseek-v4.1-flash + spellout deepseek/deepseek-v4.1-flash + taskname deepseek/deepseek-v4.1-flash + title deepseek/deepseek-v4.1-flash + + roles · worker + worker deepseek/deepseek-v4.1-flash + + roles · checker + checker deepseek/deepseek-v4.1-flash + careful deepseek/deepseek-v4.1-flash + repair deepseek/deepseek-v4.1-flash + shaper deepseek/deepseek-v4.1-flash + vision deepseek/deepseek-v4.1-flash + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/09-providers-80c.png b/docs/design/settings-clarity/before/09-providers-80c.png new file mode 100644 index 0000000000..ba9830b56e Binary files /dev/null and b/docs/design/settings-clarity/before/09-providers-80c.png differ diff --git a/docs/design/settings-clarity/before/09-providers-80c.txt b/docs/design/settings-clarity/before/09-providers-80c.txt new file mode 100644 index 0000000000..e79567e04e --- /dev/null +++ b/docs/design/settings-clarity/before/09-providers-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + … Display Spending Safety Tasks Teams Providers Connections + + › type to search + your model deepseek/deepseek-v4.1-flash + the model you are talking to. Everything below it is a model codeaf uses + on your behalf. + + routing + host auto + speed guard on + routing simple + prompt profile auto + reflex • deepseek/deepseek-v4.1-flash + small work • deepseek/deepseek-v4.1-flash + seats auto · 3 pinned · models all · 1 of 1 providers · per task $5 + pinned roles none + + roles · reflex + reflex deepseek/deepseek-v4.1-flash + + roles · small work + caption deepseek/deepseek-v4.1-flash + consolidate deepseek/deepseek-v4.1-flash + guardian deepseek/deepseek-v4.1-flash + intake deepseek/deepseek-v4.1-flash + jobname deepseek/deepseek-v4.1-flash + router deepseek/deepseek-v4.1-flash + sentinel deepseek/deepseek-v4.1-flash + spellout deepseek/deepseek-v4.1-flash + taskname deepseek/deepseek-v4.1-flash + title deepseek/deepseek-v4.1-flash + + roles · worker + worker deepseek/deepseek-v4.1-flash + + roles · checker + checker deepseek/deepseek-v4.1-flash + careful deepseek/deepseek-v4.1-flash + repair deepseek/deepseek-v4.1-flash + shaper deepseek/deepseek-v4.1-flash + vision deepseek/deepseek-v4.1-flash + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/settings-clarity/before/10-connections-120c.png b/docs/design/settings-clarity/before/10-connections-120c.png new file mode 100644 index 0000000000..9fbfca644e Binary files /dev/null and b/docs/design/settings-clarity/before/10-connections-120c.png differ diff --git a/docs/design/settings-clarity/before/10-connections-120c.txt b/docs/design/settings-clarity/before/10-connections-120c.txt new file mode 100644 index 0000000000..347abae3e2 --- /dev/null +++ b/docs/design/settings-clarity/before/10-connections-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + providers + · DeepSeek key + key + · Z.ai region · key + · Moonshot region · key + · MiniMax key + · Alibaba Qwen region · key + · Codex browser + · Ollama + · Custom OpenAI-compatible API address · key + + analytics + · Amplitude key + · ChartMogul key + · Dovetail key + · Geckoboard key + · Mixpanel key + · PostHog sign in + · Segment key + + billing + · Braintree key + · Chargebee key + · ChargeOver key + · FastSpring key + · Maxio key + · Odoo key + · Paddle key + · Paddle Sandbox key + · PayPal sign in + · Rebilly key + · Recurly key + · RevenueCat key + · Stripe key + + calls & meetings + · Aircall key + · Avoma key + +─ enter opens a box for the key you already hold ─────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter takes your key · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/10-connections-80c.png b/docs/design/settings-clarity/before/10-connections-80c.png new file mode 100644 index 0000000000..f7fd3f4c2f Binary files /dev/null and b/docs/design/settings-clarity/before/10-connections-80c.png differ diff --git a/docs/design/settings-clarity/before/10-connections-80c.txt b/docs/design/settings-clarity/before/10-connections-80c.txt new file mode 100644 index 0000000000..85559dc23d --- /dev/null +++ b/docs/design/settings-clarity/before/10-connections-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + … Display Spending Safety Tasks Teams Providers Connections + + › type to search + providers + · DeepSeek key + key + · Z.ai region · key + · Moonshot region · key + · MiniMax key + · Alibaba Qwen region · key + · Codex browser + · Ollama + · Custom OpenAI-compatible API address · key + + analytics + · Amplitude key + · ChartMogul key + · Dovetail key + · Geckoboard key + · Mixpanel key + · PostHog sign in + · Segment key + + billing + · Braintree key + · Chargebee key + · ChargeOver key + · FastSpring key + · Maxio key + · Odoo key + · Paddle key + · Paddle Sandbox key + · PayPal sign in + · Rebilly key + · Recurly key + · RevenueCat key + · Stripe key + + calls & meetings + · Aircall key + · Avoma key + +─ enter opens a box for the key you already hold ─────────────────────────────── + ↑↓ move · ←→ tabs · enter takes your key · tab next place · esc close diff --git a/docs/design/settings-clarity/before/navigation-activity-120c.png b/docs/design/settings-clarity/before/navigation-activity-120c.png new file mode 100644 index 0000000000..a90a48648f Binary files /dev/null and b/docs/design/settings-clarity/before/navigation-activity-120c.png differ diff --git a/docs/design/settings-clarity/before/navigation-activity-120c.txt b/docs/design/settings-clarity/before/navigation-activity-120c.txt new file mode 100644 index 0000000000..f77e9e53a0 --- /dev/null +++ b/docs/design/settings-clarity/before/navigation-activity-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + 1 chat shift+← sep 24 – oct 7 → shift+↑ coarser +  type to filter project age ↓ │ new conversation + │ + completed │ enter open the chat + · new conversation project │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + enter go to that conversation · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/navigation-activity-80c.png b/docs/design/settings-clarity/before/navigation-activity-80c.png new file mode 100644 index 0000000000..cc030ba47a Binary files /dev/null and b/docs/design/settings-clarity/before/navigation-activity-80c.png differ diff --git a/docs/design/settings-clarity/before/navigation-activity-80c.txt b/docs/design/settings-clarity/before/navigation-activity-80c.txt new file mode 100644 index 0000000000..3393a5988c --- /dev/null +++ b/docs/design/settings-clarity/before/navigation-activity-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + 1 chat shift+← sep 24 – oct 7 → shift+↑ coarser +  type to filter project age ↓ + + completed + · new conversation project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── + enter go to that conversation · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/navigation-ai-teams-120c.png b/docs/design/settings-clarity/before/navigation-ai-teams-120c.png new file mode 100644 index 0000000000..a56e2a338f Binary files /dev/null and b/docs/design/settings-clarity/before/navigation-ai-teams-120c.png differ diff --git a/docs/design/settings-clarity/before/navigation-ai-teams-120c.txt b/docs/design/settings-clarity/before/navigation-ai-teams-120c.txt new file mode 100644 index 0000000000..4af431d080 --- /dev/null +++ b/docs/design/settings-clarity/before/navigation-ai-teams-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +  All teams │╭─ Global manager ────────────────────────────────────────────────────────────────────────────╮ + + New team ││ Optional · coordinates the managers of top-level teams │ + ││ + Global manager │ + │╰─────────────────────────────────────────────────────────────────────────────────────────────╯ + │ + │ teams ✦ Organize + │ + │ + │ A team is a set of conversations you run together; give it a manager and + │ you talk to the manager, which hands out the work and asks you only what + │ it cannot decide. + │ + │ ✦ Organize my conversations + New team + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Every team, and what waits on you from any of them · enter · tab next place diff --git a/docs/design/settings-clarity/before/navigation-ai-teams-80c.png b/docs/design/settings-clarity/before/navigation-ai-teams-80c.png new file mode 100644 index 0000000000..f6d4d38e84 Binary files /dev/null and b/docs/design/settings-clarity/before/navigation-ai-teams-80c.png differ diff --git a/docs/design/settings-clarity/before/navigation-ai-teams-80c.txt b/docs/design/settings-clarity/before/navigation-ai-teams-80c.txt new file mode 100644 index 0000000000..3b334687fd --- /dev/null +++ b/docs/design/settings-clarity/before/navigation-ai-teams-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + +  All teams │╭─ Global manager ──────────────────────────────────────╮ + + New team ││ Optional · coordinates the managers of top-level │ + ││ teams │ + ││ + Global manager │ + │╰───────────────────────────────────────────────────────╯ + │ + │ teams ✦ Organize + │ + │ + │ A team is a set of conversations you run together; + │ give it a manager and you talk to the manager, which + │ hands out the work and asks you only what it cannot + │ decide. + │ + │ ✦ Organize my conversations + New team + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +──────────────────────────────────────────────────────────────────────────────── + Every team, and what waits on you from any of them · enter · tab next place diff --git a/docs/design/settings-clarity/before/search-old-label-120c.png b/docs/design/settings-clarity/before/search-old-label-120c.png new file mode 100644 index 0000000000..ea604b2cc4 Binary files /dev/null and b/docs/design/settings-clarity/before/search-old-label-120c.png differ diff --git a/docs/design/settings-clarity/before/search-old-label-120c.txt b/docs/design/settings-clarity/before/search-old-label-120c.txt new file mode 100644 index 0000000000..1d668158f2 --- /dev/null +++ b/docs/design/settings-clarity/before/search-old-label-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › memory floor + nothing matches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ the accounts codeaf may reach for you ──────────────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter act · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/search-old-label-80c.png b/docs/design/settings-clarity/before/search-old-label-80c.png new file mode 100644 index 0000000000..6e84f7de6b Binary files /dev/null and b/docs/design/settings-clarity/before/search-old-label-80c.png differ diff --git a/docs/design/settings-clarity/before/search-old-label-80c.txt b/docs/design/settings-clarity/before/search-old-label-80c.txt new file mode 100644 index 0000000000..b5f1137f08 --- /dev/null +++ b/docs/design/settings-clarity/before/search-old-label-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + … Display Spending Safety Tasks Teams Providers Connections + + › memory floor + nothing matches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ the accounts codeaf may reach for you ──────────────────────────────────────── + ↑↓ move · ←→ tabs · enter act · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/search-privacy-120c.png b/docs/design/settings-clarity/before/search-privacy-120c.png new file mode 100644 index 0000000000..04a331446b Binary files /dev/null and b/docs/design/settings-clarity/before/search-privacy-120c.png differ diff --git a/docs/design/settings-clarity/before/search-privacy-120c.txt b/docs/design/settings-clarity/before/search-privacy-120c.txt new file mode 100644 index 0000000000..3d957fbe45 --- /dev/null +++ b/docs/design/settings-clarity/before/search-privacy-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › privacy + nothing matches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ the accounts codeaf may reach for you ──────────────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter act · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/search-privacy-80c.png b/docs/design/settings-clarity/before/search-privacy-80c.png new file mode 100644 index 0000000000..901fdb1d4f Binary files /dev/null and b/docs/design/settings-clarity/before/search-privacy-80c.png differ diff --git a/docs/design/settings-clarity/before/search-privacy-80c.txt b/docs/design/settings-clarity/before/search-privacy-80c.txt new file mode 100644 index 0000000000..c2351b7949 --- /dev/null +++ b/docs/design/settings-clarity/before/search-privacy-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + … Display Spending Safety Tasks Teams Providers Connections + + › privacy + nothing matches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ the accounts codeaf may reach for you ──────────────────────────────────────── + ↑↓ move · ←→ tabs · enter act · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/search-service-120c.png b/docs/design/settings-clarity/before/search-service-120c.png new file mode 100644 index 0000000000..d6a9b2f600 Binary files /dev/null and b/docs/design/settings-clarity/before/search-service-120c.png differ diff --git a/docs/design/settings-clarity/before/search-service-120c.txt b/docs/design/settings-clarity/before/search-service-120c.txt new file mode 100644 index 0000000000..d9690bc7c7 --- /dev/null +++ b/docs/design/settings-clarity/before/search-service-120c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:28pm + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › github + nothing matches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ the accounts codeaf may reach for you ──────────────────────────────────────────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter act · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/before/search-service-80c.png b/docs/design/settings-clarity/before/search-service-80c.png new file mode 100644 index 0000000000..7fdd6b1713 Binary files /dev/null and b/docs/design/settings-clarity/before/search-service-80c.png differ diff --git a/docs/design/settings-clarity/before/search-service-80c.txt b/docs/design/settings-clarity/before/search-service-80c.txt new file mode 100644 index 0000000000..7da657b2d8 --- /dev/null +++ b/docs/design/settings-clarity/before/search-service-80c.txt @@ -0,0 +1,48 @@ + >● codeaf home teams chats sessions spend settings wed 2:29pm + +──────────────────────────────────────────────────────────────────────────────── + + … Display Spending Safety Tasks Teams Providers Connections + + › github + nothing matches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +─ the accounts codeaf may reach for you ──────────────────────────────────────── + ↑↓ move · ←→ tabs · enter act · type to filter · tab next place · esc close diff --git a/docs/design/settings-clarity/capture.py b/docs/design/settings-clarity/capture.py new file mode 100644 index 0000000000..f65bde92d1 --- /dev/null +++ b/docs/design/settings-clarity/capture.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Capture real settings in isolated profiles; no model calls or user data.""" +import argparse, json, os, pathlib, shlex, subprocess, tempfile, time +p=argparse.ArgumentParser(); p.add_argument('--binary',required=True);p.add_argument('--out',required=True);p.add_argument('--version',choices=['before','after'],required=True);p.add_argument('--columns',type=int,default=120) +a=p.parse_args();out=pathlib.Path(a.out);out.mkdir(parents=True,exist_ok=True) +sock='settings-proof-'+str(os.getpid()) +def tm(*args,check=True):return subprocess.run(['tmux','-L',sock,*args],text=True,capture_output=True,check=check).stdout +def key(*keys):tm('send-keys','-t','proof',*keys);time.sleep(.15) +def text(s):tm('send-keys','-t','proof','-l',s);time.sleep(.15) +def click_word(word): + lines=tm('capture-pane','-p','-t','proof').splitlines() + matches=[(line.index(word)+1,i+1) for i,line in enumerate(lines) if word in line] + if len(matches)!=1:raise RuntimeError('ambiguous mouse target '+word+': '+str(matches)) + x,y=matches[0] + for suffix in ('M','m'): + seq=f'\x1b[<0;{x};{y}{suffix}' + tm('send-keys','-t','proof','-H',*[f'{b:02x}' for b in seq.encode()]) + time.sleep(.2) +def grab(name): + time.sleep(.25) + plain=tm('capture-pane','-p','-t','proof');paint=tm('capture-pane','-p','-e','-t','proof') + stem=out/(name+'-'+str(a.columns)+'c');stem.with_suffix('.txt').write_text(plain);stem.with_suffix('.ansi').write_text(paint) + if 'settings' not in plain.lower():raise RuntimeError('settings did not open: '+plain) + print(stem,flush=True) + return plain +with tempfile.TemporaryDirectory(prefix='codeaf-settings-proof-', ignore_cleanup_errors=True) as tmp: + root=pathlib.Path(tmp);profile=root/'profile';profile.mkdir();work=root/'project';work.mkdir() + cfg={'setup_seen_at':'2026-10-07T00:00:00Z','api_key':'demo-key-not-used','model.talk':'deepseek/deepseek-v4.1-flash','ui.mouse':'on'} + for role in ('high','low','worker','reflex','mastermind'):cfg['models.tiers.'+role]='deepseek/deepseek-v4.1-flash' + (profile/'config.json').write_text(json.dumps(cfg)) + env={'HOME':str(profile),'CODEAF_HOME':str(profile),'CODEAF_PROFILE_DIR':str(profile),'CODEAF_NO_UPDATE_CHECK':'1','CODEAF_BASE_URL':'http://127.0.0.1:9','TERM':'xterm-256color','LANG':'C.UTF-8','PATH':'/usr/bin:/bin'} + command='cd '+shlex.quote(str(work))+' && env -i '+ ' '.join(shlex.quote(k+'='+v) for k,v in env.items())+' '+shlex.quote(a.binary)+' chat --no-host' + try: + tm('new-session','-d','-s','proof','-x',str(a.columns),'-y','48',command);time.sleep(2) + text('/settings');key('Enter');time.sleep(.8) + names=['session','context','workspace','display','spending','safety','tasks','teams','providers','connections'] if a.version=='before' else ['general','models','memory','tasks','ai-teams','permissions','spending','connections','privacy'] + for i,name in enumerate(names): + frame=grab(f'{i+1:02}-{name}') + if a.version=='after' and 'Advanced' in frame: + key('Home') + for _ in range(35): + current=tm('capture-pane','-p','-t','proof') + if 'Less common controls' in current: + key('Enter');grab(f'{i+1:02}-{name}-advanced');key('End');grab(f'{i+1:02}-{name}-advanced-end');break + key('Down') + else:raise RuntimeError('could not reach Advanced: '+name) + key('Right') + for name,query in [('search-privacy','privacy'),('search-old-label','memory floor'),('search-service','github')]: + key('C-u');text(query);grab(name) + if a.version=='after': + key('C-u');text('ui.hints');grab('edit-hints-before');key('Enter');grab('edit-hints-after') + saved=json.loads((profile/'config.json').read_text()) + if saved.get('ui.hints') is not False:raise RuntimeError('hints preference did not persist') + key('C-u');text('ui.work');key('Enter');grab('choice-current') + original=json.loads((profile/'config.json').read_text()).get('ui.work') + key('Down');grab('choice-highlighted');key('Escape') + if json.loads((profile/'config.json').read_text()).get('ui.work')!=original:raise RuntimeError('cancelled choice changed preference') + key('Enter');key('Down');key('Enter');grab('choice-saved') + if json.loads((profile/'config.json').read_text()).get('ui.work')!='open':raise RuntimeError('selected choice did not persist') + key('Enter');grab('choice-reopened');key('Escape') + key('C-u');text('task.parallel');key('Enter');grab('edit-number-open') + key('C-u');text('7');click_word('Cancel');grab('edit-number-cancelled') + if json.loads((profile/'config.json').read_text()).get('task.parallel',0)!=0:raise RuntimeError('cancelled draft changed task limit') + key('Enter');key('C-u');text('invalid');key('Enter');grab('edit-invalid-value') + if json.loads((profile/'config.json').read_text()).get('task.parallel',0)!=0:raise RuntimeError('invalid draft changed saved task limit') + key('C-u');text('4');click_word('Save');grab('edit-limit-saved') + if json.loads((profile/'config.json').read_text()).get('task.parallel')!=4:raise RuntimeError('valid task limit did not persist') + key('Escape');key('Escape');text('/settings');key('Enter');text('task.parallel');key('Enter');grab('edit-limit-reopened') + key('C-u');key('Enter');grab('edit-limit-cleared') + if json.loads((profile/'config.json').read_text()).get('task.parallel')!=0:raise RuntimeError('blank task limit did not restore no limit') + key('Enter');grab('edit-limit-unlimited');key('Escape') + key('C-u') + for _ in range(9):key('Left') + # SGR mouse input reaches the real terminal event decoder. + if a.version=='after' and a.columns>=100: + seq='\x1b[<35;5;6M' + tm('send-keys','-t','proof','-H',*[f'{b:02x}' for b in seq.encode()]);grab('hover-category') + key('Escape');text('/teams');key('Enter');time.sleep(.4);grab('navigation-ai-teams') + key('Escape');text('/history');key('Enter');time.sleep(.4);grab('navigation-activity') + finally: + tm('send-keys','-t','proof','C-c',check=False);time.sleep(.6);tm('kill-server',check=False);time.sleep(.3) diff --git a/docs/design/settings-clarity/render.py b/docs/design/settings-clarity/render.py new file mode 100644 index 0000000000..b38de43650 --- /dev/null +++ b/docs/design/settings-clarity/render.py @@ -0,0 +1,8 @@ +import pathlib,json,subprocess,re,sys +root=pathlib.Path(sys.argv[1]) +for source in sorted(root.glob('*.ansi')): + cols=int(re.search(r'-(\d+)c$',source.stem)[1]);lines=source.read_text().splitlines() + payload='\x1b[?25l\x1b[?7l\x1b[2J\x1b[H'+''.join(f'\x1b[{i+1};1H'+line for i,line in enumerate(lines)) + cast=source.with_suffix('.cast');cast.write_text(json.dumps({'version':2,'width':cols,'height':len(lines),'env':{'TERM':'xterm-256color'}})+'\n'+json.dumps([.01,'o',payload])+'\n'+json.dumps([.1,'o',''])+'\n') + subprocess.run(['agg','--quiet','--font-family','JetBrainsMono Nerd Font Mono','--font-size','16','--theme','dracula','--last-frame-duration','.2','--select','100%',str(cast),str(source.with_suffix('.gif'))],check=True) + subprocess.run(['ffmpeg','-loglevel','error','-y','-i',str(source.with_suffix('.gif')),'-frames:v','1',str(source.with_suffix('.png'))],check=True) diff --git a/docs/design/settings-clarity/verification.md b/docs/design/settings-clarity/verification.md new file mode 100644 index 0000000000..c79c7c4a1c --- /dev/null +++ b/docs/design/settings-clarity/verification.md @@ -0,0 +1,80 @@ +# Verification + +All builds, full suites and acceptance runs used Spark. No full suite or model call ran on the laptop. Parallel review and implementation used the native subagents; no design or review was delegated to an external model. + +## Revisions and evidence + +- Baseline screenshots: `4fd9cfaa0fb63b1c78db5aa6ebe9479e512d1640`. +- Initial screenshot source: `24d8d436f5003fd8a8a0eec098943df0d130802c`. +- Full `make pr-ready` passed on `ec65c91ccc3f2ba87f189c4842abb9f4672f7538`, Spark job `20261007-185348-001853-settings-final-proof`. Build, vet, packed/manual checks, 139 layout/law files across 30 packages, and complete affected packages (`cmd/codeaf`, `internal/config`, `internal/manual`, `internal/tui3`) passed. All selected package tests passed on their first run. +- After that run, the final editor adjustment moved validation beside the input and explained empty optional counts. Its rendered-frame, preservation, valid-save, reopen, and clear tests passed at both 80 and 120 columns: `20261007-185636-001854`. +- Everyday task controls were then placed first, with task analysis and shell timing under Advanced. Focused settings/configuration tests and all 67 final captures passed in `20261007-190236-001856`. Both widths verify the task order, adjacent validation, valid save/reopen and blank clear/reopen. +- The complete source plus gallery at `569cf41e6368e847ad28ba29063cf8551fb8815b` also passed `make pr-ready` in `20261007-190655-001857-settings-pr1792-acceptance`, with every selected test passing on its first run. +- The branch was subsequently rebased onto dev `6246d7154` (new built-in review/security programs). The sole conflict was manual wording; both the new program badges and Activity terminology were retained. Settings rendering and presentation source are identical to the screenshot revision. +- The final full acceptance result, including the gallery commit and its exact tested revision, is recorded in [draft PR #1792](https://github.com/Agent-Field/CodeAF/pull/1792). This file is written before that run to keep its source frozen during testing. + +Raw logs and screenshot provenance remain on Spark under `/home/santosh/settings-redesign-evidence-20261007/`; fleet logs retain each job's command and exit status. + +## Examples checked + +| Example | Evidence | +| --- | --- | +| Open an old profile with memory off, hints off, a task limit, team preferences and an unknown future field | Config integration tests verify unchanged bytes on read and preserve other fields on a setting write | +| Find a moved setting by its old name or config key | Global search tests and before/after old-label captures | +| Find an account while browsing another category | Global catalog search and asynchronous account-refresh regression tests | +| Inspect memories without pretending to change a preference | Real Memory frame and action-hint test; Enter opens the existing memory view | +| Use the keyboard or hover a row for the same explanation | Stable geometry, shared focused-help and pointer target tests; hover capture | +| Enter `invalid` for concurrent tasks | Deliberate error fixture; rejection is adjacent to the draft and the stored limit stays unchanged | +| Correct the draft to `4`, save, close settings, and reopen | Real CLI capture asserts the persisted JSON and shows the reopened value | +| Clear the task limit | Real CLI capture asserts persisted zero and shows the reopened no-limit state | +| Edit a credential | Masked value/caret tests; modal pointer input cannot activate controls behind the editor | +| Choose local/free/OCR document reading | Four-choice real launch-boundary test verifies the preference reaches the session configuration | +| Use a task cap and the default task model | Spark `20261007-185331-001852-settings-task-wiring` passes task-slot, model fallback, machine admission and queuing checks | +| Use plain/narrow terminals | Navigation laws preserve active labels, More, hit targets and text-only navigation; 80-column real frames accompany all category captures | + +## Failure analysis retained + +The first complete run caught stale assumptions about categories, labels and Advanced disclosure, plus real defects: omitted custom-provider controls, account refresh lost while searching another category, lost Advanced state on returning from the crew panel, and a legacy practice command pointing at a hidden row. These were corrected while retaining behavior assertions. + +Credit-warning tests failed identically on the base and changed revisions when host credentials overrode their fixture. Both passed with those environment keys unset. The fixture now isolates its own credentials; all 18 credit tests passed even with deliberately conflicting dummy environment keys (`20261007-183854-001835-credits-fixture-fix`). Runtime credential precedence is unchanged. + +## Limits + +This verifies the settings surface, persistence and named runtime boundaries. It does not claim live end-to-end coverage of every external provider, OAuth service or billing account. Screenshots use dummy data and a loopback endpoint. Settings captured at process startup still require restarting already-open CLI chats, as their help states. The shared resident registry and CLI configuration vocabulary remain compatible; this is a redesign of the live chat surface. + +The PR remains a draft for owner review. These changes are not installed or merged by this task. + +## Interaction refinement + +Source revision: `746af81a7b9af9ba3839d8674dacb508678d6f13`. +Final clean build and refreshed real CLI screenshots: Spark job +`20261007-202643-001862-settings-interaction-final`. +An earlier successful capture (`001861`) prompted one final correction: an editor +opened through global search highlights its setting's category while preserving +the search on return. + +Focused Spark checks cover explicit choice preview/cancel/save/reopen, mouse +save/cancel, unchanged invalid drafts, successful-save receipts, pending remote +Close wording, 24-column controls and short terminals, approval-gate updates, +masked editors, and paste/wheel isolation. The real CLI capture sends terminal +mouse events to Save and Cancel and asserts the stored profile values. + +Full acceptance for the final source and refreshed gallery is recorded in the +PR with its exact revision and fleet job; no local full suite was run. + +## Chat settings integration + +Chat tools now use the presentation metadata and shared choice labels. Focused +tool tests verify new and legacy search names, exact accepted values, real +registry writes/readback, persisted hints/work/memory values, protected/invalid +write preservation, hidden controls and restart/project-override receipts. + +Spark `20261007-204140-001865-settings-live-ui` passed the live interface refresh +regressions and related choice/hints/word tests. Full acceptance for the final +revision, including the affected session package, is recorded in the draft PR. +These are direct tool and runtime-boundary tests, not a claim that a live model +conversation was used for this validation. + +Chat-written routing, speed guard and pinned-host preferences explicitly recommend +a restart to ensure activation in already-open chats. This wording is specific +to the tool path; the panel retains its live transport update hooks. diff --git a/internal/config/settings_choice_words.go b/internal/config/settings_choice_words.go new file mode 100644 index 0000000000..8836af959e --- /dev/null +++ b/internal/config/settings_choice_words.go @@ -0,0 +1,53 @@ +package config + +// ChatChoiceLabel translates only the display. Raw registry values and writers +// retain their compatibility semantics, including inverse ui.hints. +func ChatChoiceLabel(key, raw, fallback string) string { + switch key { + case KeyHints: + if raw == "on" { + return "off" + } + if raw == "off" { + return "on" + } + case KeyTaskStart: + switch raw { + case "sized": + return "assess while working" + case "single": + return "skip assessment" + } + case KeyTaskSettle: + switch raw { + case "ask": + return "ask me" + case "auto": + return "let the chat decide" + } + case KeyToolApprovalMode: + switch raw { + case "allow": + return "allow by default" + case "prompt": + return "ask by default" + case "deny": + return "block by default" + } + case KeyWork: + switch raw { + case "fold": + return "collapsed" + case "open": + return "expanded" + } + case KeyModelPool: + switch raw { + case "on": + return "use and contribute" + case "read": + return "use only" + } + } + return fallback +} diff --git a/internal/config/settings_presentation.go b/internal/config/settings_presentation.go new file mode 100644 index 0000000000..7116aecc05 --- /dev/null +++ b/internal/config/settings_presentation.go @@ -0,0 +1,229 @@ +package config + +// SettingPresentation describes the live chat settings surface. It deliberately +// does not change the shared registry or persisted keys: resident users and old +// profiles keep the same readers, validators and writers. +type SettingPresentation struct { + Category string + Label string + Description string + Aliases []string + Advanced bool + Hidden bool + Scope string + Activation string +} + +// ChatPresentation is a pure view of a setting, never a configuration migration. +// Empty Label or Description lets a surface retain its existing wording. Timing +// is stated only where the consumer establishes it; no blanket "applies now". +func (s Setting) ChatPresentation() SettingPresentation { + p := SettingPresentation{Scope: "This profile", Aliases: []string{s.Label, s.Category, s.Key}} + switch s.Category { + case CategoryModels: + p.Category = "Models" + case CategorySpending: + p.Category = "Spending" + case CategorySafety: + p.Category = "Permissions" + case CategoryTasks: + p.Category = "Tasks" + case CategoryTeams: + p.Category = "AI teams" + p.Scope = "Default for AI teams; teams can override it" + case CategoryPractice, CategoryInterface: + p.Category = "General" + } + if ProjectKeyAllowed(s.Key) { + p.Scope = "Profile default; a project can override it" + } + if s.Kind == SettingModel && s.Slot == "talk" { + p.Scope = "This chat and the profile default" + } + switch s.Key { + case KeyPracticeIdle, KeyPracticeBudget, KeyBriefAfter, KeyTenureAfter, KeySplitPct: + p.Hidden = true // Resident-only controls have no live chat consumer. + case KeyMemoryEnabled: + p.Category, p.Label = "Memory", "remember across chats" + p.Description = "Learns useful preferences and corrections from conversations and recalls relevant memories. /memories lets you inspect what is saved. Turning this off does not delete saved memories." + p.Activation = "Restart the CLI to apply this to already-open chats" + case KeyStandingBackground: + p.Label = "background reminders" + p.Description = "Check reminders, watches and routines while the CLI is closed. Uses a timer under your login; the machine must be awake and you must be logged in." + case KeyUpdateAuto: + p.Label = "automatic updates" + case KeyMouse: + p.Label = "mouse interaction" + case KeyWork: + p.Label = "completed tool details" + p.Description = "Collapsed keeps completed tool steps behind one summary. Expanded keeps those details visible." + case KeyIcons: + p.Label = "tool icons" + case KeyTaskColumn: + p.Label = "task sidebar" + case KeyQuickSwitch: + p.Label = "switch chats immediately" + case KeyHints: + p.Label = "show hints" + p.Description = "Show contextual tips while you work." + case KeyAttributionModel: + p.Label = "include model in commit attribution" + case KeyHistoryEnabled: + p.Category, p.Label = "Privacy", "save input history" + case KeyDraftPersist: + p.Category, p.Label = "Privacy", "save unfinished drafts" + case KeyTelemetry: + p.Category, p.Label = "Privacy", "share anonymous usage reports" + case KeyModelPool: + p.Category, p.Label = "Privacy", "shared model recommendations" + p.Description = "Use and contribute receives model recommendations and shares text-free performance measurements. Use only sends nothing. Off does neither. Code, prompts and file paths are never included." + case KeyModelPoolPublicKey: + p.Category, p.Label, p.Advanced = "Privacy", "model pool signing key", true + case KeyToolApprovalMode: + p.Label = "tool approvals" + p.Description = "Allow by default runs tools without asking. Ask by default requests approval except for safe read-only calls and tools that ask for permission themselves. Block by default refuses tools. Tool exceptions override this choice; dangerous shell commands still require approval." + case KeyToolApprovals: + p.Label = "tool exceptions" + case KeyGuardian: + p.Label = "AI approval screening" + case KeyConsentTimeout: + p.Label, p.Advanced = "pause approval timer after", true + case KeyBashBackgroundAfter: + p.Category, p.Label, p.Advanced = "Tasks", "background shell commands after", true + case KeyTaskAutoApprove: + p.Category, p.Label = "Permissions", "start proposed tasks after" + case KeyTaskSettle: + p.Category, p.Label = "Tasks", "when task results need a decision" + p.Description = "Ask me leaves the decision with you. Let the chat decide reviews the work and asks you only when it cannot decide." + case KeyTaskStart: + p.Label, p.Advanced = "task scope assessment", true + p.Description = "Assesses the brief while the worker starts, helping it identify work to delegate. Skip assessment avoids that extra model call. Both start work immediately and can delegate parts already listed in the brief." + case KeyTaskAudit: + p.Label = "review task results" + case KeyTaskRepairRounds: + p.Label = "repair attempts" + case KeyTaskParallel: + p.Label = "concurrent tasks" + case KeyTaskMaxLoad: + p.Label, p.Advanced = "maximum load per CPU core", true + case KeyTaskMinFreeMB: + p.Label, p.Advanced = "minimum available RAM", true + case KeyTeamsQuestionsUp: + p.Label = "ask the AI manager first" + case KeyTeamsWake: + p.Label = "run when team messages arrive" + case KeyTeamsCapUSDDay: + p.Label = "daily spending limit" + case KeyTeamsDepthLimit: + p.Label, p.Advanced = "maximum team nesting", true + case KeyTeamsSubSharePct: + p.Label, p.Advanced = "new subteam budget share", true + p.Activation = "Applies to newly created subteams only" + case KeyDailyBudget: + p.Label = "daily spending limit" + case KeySpendRail: + p.Label = "per-chat spending limit" + case KeyPlanConsent: + p.Label = "ask before a plan costs more than" + case KeyVisionModel: + p.Label = "image understanding model" + case KeyDocumentEngine: + p.Label = "document reader" + case KeyEffort: + p.Label = "reasoning effort" + case KeySearchProvider: + p.Category, p.Label = "Connections", "web search provider" + case KeyExaKey, KeyFirecrawlKey, KeyJinaKey: + p.Category = "Connections" + case KeyAPIKey: + p.Category = "Connections" + case KeyGoogleOAuthClient, KeyGoogleOAuthSecret, KeySlackOAuthClient: + p.Category, p.Advanced = "Connections", true + case KeySSHControlPersist: + p.Category, p.Label, p.Advanced = "Connections", "SSH connection reuse", true + case KeySSHServerAlive: + p.Category, p.Label, p.Advanced = "Connections", "SSH keepalive interval", true + case KeySSHServerMisses: + p.Category, p.Label, p.Advanced = "Connections", "SSH missed keepalives", true + case KeySSHIPQoS: + p.Category, p.Label, p.Advanced = "Connections", "SSH traffic priority", true + case KeyContextFill: + p.Label, p.Advanced = "compact context at", true + case KeyCompletionReserve: + p.Label, p.Advanced = "tokens reserved for replies", true + case KeyWorkingSet: + p.Label, p.Advanced = "working context limit", true + case KeyContextReuse: + p.Label, p.Advanced = "context reuse limit", true + case KeyRouting, KeyPromptProfile, KeyLaneGuard, KeyModelRoles, KeyTierReflexModel, KeyTierLowModel, KeyTierWorkerModel, KeyTierHighModel, KeyTierMastermindModel, KeyReplyGuard: + p.Advanced = true + } + // Preserve the former navigation names as search aliases for people who + // learned them before the categories were reorganized. + switch s.Category { + case CategoryModels: + p.Aliases = append(p.Aliases, "Providers", "Context") + case CategorySafety: + p.Aliases = append(p.Aliases, "Safety") + case CategoryInterface: + p.Aliases = append(p.Aliases, "Display", "Workspace") + case CategoryPractice: + p.Aliases = append(p.Aliases, "Session", "Workspace") + } + if s.Key == LaneSettingKey(LaneSlotTalk) { + p.Label = "model provider" + p.Advanced = true + } + if s.Kind == SettingModel { + switch s.Slot { + case "talk": + p.Label = "chat model" + case "image": + p.Label = "image generation" + case "speech": + p.Label = "speech generation" + case "music": + p.Label = "music generation" + case "video": + p.Label = "video generation" + case "voice": + p.Label = "audio transcription" + } + + if slot, ok := ModelSlotFor(s.Slot); ok && slot.Role != "" && s.Slot != "talk" { + p.Hidden = true // Legacy role picker cannot apply to a v3 engine. + } + } + switch s.Key { + case KeyEffort, KeyModelFallbacks, KeyTaskAutoApprove, KeyBashBackgroundAfter, + KeyTaskModel, KeyTaskAudit, KeyAttributionModel, KeyReplyGuard, + KeyPromptProfile, KeyTaskSettle, KeyTaskRepairRounds, KeyTaskParallel, + KeyHistoryEnabled, KeyDraftPersist, KeyDocumentEngine, KeyTaskColumn, + KeyGoogleOAuthClient, KeyGoogleOAuthSecret, KeySlackOAuthClient, + KeySSHControlPersist, KeySSHServerAlive, KeySSHServerMisses, KeySSHIPQoS: + p.Activation = "Restart the CLI to apply this to already-open chats" + case KeyTelemetry: + p.Activation = "Off stops sending now; turning on applies next CLI launch" + case KeySearchProvider, KeyExaKey, KeyFirecrawlKey, KeyJinaKey: + p.Activation = "Applies on the next web search" + } + switch s.Key { + case KeyMemoryEnabled: + p.Aliases = append(p.Aliases, "remember", "learn", "forget", "recall") + case KeyTelemetry: + p.Aliases = append(p.Aliases, "analytics", "tracking", "privacy", "data sharing") + case KeyTaskMinFreeMB: + p.Aliases = append(p.Aliases, "RAM", "memory", "resources") + case KeyTaskMaxLoad: + p.Aliases = append(p.Aliases, "CPU", "resources") + case KeyUpdateAuto: + p.Aliases = append(p.Aliases, "updates", "upgrade", "version") + case KeyAPIKey, KeyExaKey, KeyFirecrawlKey, KeyJinaKey: + p.Aliases = append(p.Aliases, "API key", "credentials") + case KeyDailyBudget, KeySpendRail, KeyTeamsCapUSDDay, KeyPlanConsent: + p.Aliases = append(p.Aliases, "budget", "cost", "limit") + case KeyHistoryEnabled, KeyDraftPersist: + p.Aliases = append(p.Aliases, "privacy", "saved messages") + } + return p +} diff --git a/internal/config/settings_presentation_test.go b/internal/config/settings_presentation_test.go new file mode 100644 index 0000000000..2683d24106 --- /dev/null +++ b/internal/config/settings_presentation_test.go @@ -0,0 +1,119 @@ +package config + +import ( + "bytes" + "encoding/json" + "os" + "slices" + "testing" +) + +func TestChatPresentationLeavesExistingProfileUntouched(t *testing.T) { + dir := t.TempDir() + existing := []byte(`{"ui.hints":false,"memory.enabled":"off","task.parallel":4,"teams.wake":false,"practice_idle":"31m","future.setting":{"nested":[1,"keep"]}}`) + if err := os.WriteFile(BudgetConfigPath(dir), existing, 0600); err != nil { + t.Fatal(err) + } + rows := registry(t, dir).Rows() + for _, row := range rows { + _ = row.ChatPresentation() + } + got, err := os.ReadFile(BudgetConfigPath(dir)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, existing) { + t.Fatalf("reading the new categories rewrote the existing profile: %s", got) + } + if TaskParallelAt(dir) != 4 || TeamDefaultsAt(dir).Wake || HintsAt(dir) || MemoryEnabledAt(dir) { + t.Fatal("existing preferences were reset") + } + row, _ := registry(t, dir).Row(KeyTaskParallel) + if err := row.Apply("6"); err != nil { + t.Fatal(err) + } + got, err = os.ReadFile(BudgetConfigPath(dir)) + if err != nil { + t.Fatal(err) + } + var before, after map[string]json.RawMessage + if err := json.Unmarshal(existing, &before); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(got, &after); err != nil { + t.Fatal(err) + } + for key, value := range before { + if key == KeyTaskParallel { + continue + } + var a, b bytes.Buffer + if err := json.Compact(&a, value); err != nil { + t.Fatal(err) + } + if err := json.Compact(&b, after[key]); err != nil { + t.Fatal(err) + } + if !bytes.Equal(a.Bytes(), b.Bytes()) { + t.Errorf("editing concurrency changed %s: %s -> %s", key, value, after[key]) + } + } + if TaskParallelAt(dir) != 6 { + t.Fatal("newly saved concurrency not used by resolver") + } +} + +func TestChatPresentationPreservesHiddenResidentSettings(t *testing.T) { + r := registry(t, t.TempDir()) + for _, key := range []string{KeyPracticeIdle, KeyPracticeBudget, KeyBriefAfter, KeyTenureAfter} { + row, ok := r.Row(key) + if !ok || !row.ChatPresentation().Hidden { + t.Fatalf("%s must remain registered but hidden from chat", key) + } + } + for _, slot := range ModelSlots() { + row, ok := r.Row(ModelSettingKey(slot.Slot)) + if !ok { + t.Fatal(slot.Slot) + } + wantHidden := slot.Role != "" && slot.Slot != "talk" + if row.ChatPresentation().Hidden != wantHidden { + t.Errorf("slot %s visibility incompatible with live picker", slot.Slot) + } + } +} + +func TestChatPresentationKeepsAliasesAndEveryVisibleRowCategorized(t *testing.T) { + categories := []string{"General", "Models", "Memory", "Tasks", "AI teams", "Permissions", "Spending", "Connections", "Privacy"} + for _, row := range registry(t, t.TempDir()).Rows() { + p := row.ChatPresentation() + if !p.Hidden && !slices.Contains(categories, p.Category) { + t.Errorf("%s has unknown category %q", row.Key, p.Category) + } + if !slices.Contains(p.Aliases, row.Label) || !slices.Contains(p.Aliases, row.Key) { + t.Errorf("%s lost legacy search terms", row.Key) + } + } +} + +func TestChatPresentationDoesNotInvertPersistedHints(t *testing.T) { + dir := t.TempDir() + row, _ := registry(t, dir).Row(KeyHints) + if row.ChatPresentation().Label != "show hints" { + t.Fatal("missing positive display label") + } + // The shared registry still answers the resident's negative question. The + // chat surface alone reverses its display; storage remains positive. + if err := row.Apply("on"); err != nil { + t.Fatal(err) + } + if HintsAt(dir) { + t.Fatal("disable-hints on must preserve ui.hints=false") + } + if err := row.Apply("off"); err != nil { + t.Fatal(err) + } + if !HintsAt(dir) { + t.Fatal("disable-hints off must preserve ui.hints=true") + } +} diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index 6905c25f94..6eabae9741 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -301,14 +301,14 @@ Canonical word, the other words it answers to, its argument form, and what it do | `/crew` | — | `models ` | which models a seat may be picked from — `all`, `open`, `≤in/out`, ids | | `/crew` | — | `cap ` | the most tasks' crews may spend in a day | | `/crew` | — | `cap task ` | the most one task may spend — $5 unless set; `-yes-spend` does not lift it | -| `/task` | — | — | opens the full-screen sessions place — the same place as `/history` and ctrl+. | +| `/task` | — | — | opens the full-screen activity page — the same place as `/history` and ctrl+. | | `/task` | — | `` | starts one worker at once; its brief is written and its width read beside it, and wide work splits | | `/task` | — | `solo ` | starts one worker at once, with no reading of its width | | `/task` | — | `--best ` | starts the task on the strongest crew the allowed models make, this task only | | `/task` | — | `--cheap ` | starts the task on the cheapest crew that does the work, this task only | | `/stop` | — | — | asks before stopping the open task or selected work; with no target says `open a running task to stop it` | | `/redo` | — | `stronger` | runs the last task again on a stronger crew, and teaches the crew that kind of work needs more | -| `/history` | — | — | opens the full-screen sessions place — every task this machine has run, filterable (also ctrl+.) | +| `/history` | — | — | opens the full-screen activity page — every task this machine has run, filterable (also ctrl+.) | ## Status, search, teams and spending commands @@ -370,7 +370,7 @@ Under the table `/help` prints the keys that have no slash command, including rows, directly under the `tab` row: ``` -alt+1…8 go to a place · in the tab bar's own order: home teams chats sessions spend settings standing memory +alt+1…8 go to a place · established shortcut order: home · AI teams · chats · activity · spend · settings · standing · memory alt+. on a place: what else is here · every key that place has, drawn on a place, tab is the next place · esc back ``` @@ -1548,7 +1548,7 @@ The row is called by the first words you typed for a second or two, and then by name a small model gives it. When a `/task` runs on the run engine (*The worker harness* page), the row it writes to the -sessions place carries the step its worker is on **right now** — the running glyph `◐`, the shell +activity page carries the step its worker is on **right now** — the running glyph `◐`, the shell lead `$` and the command — with the task's `N steps · $0.11` under it. The line is there only while a step is in flight, and goes the moment the command ends. @@ -1556,10 +1556,10 @@ while a step is in flight, and goes the moment the command ends. strips the tag and takes the remaining words through this same road. Backspace immediately after the tag makes it plain prose. -**A bare `/task` opens the full-screen sessions place** — the same place `/history` and `ctrl+.` +**A bare `/task` opens the full-screen activity page** — the same place `/history` and `ctrl+.` open, holding this machine's conversation and task record. It does *not* print a usage line, and it starts nothing. On a project that has never run one it opens the page anyway, headed -`sessions` over one line: `work you send off with /task lands here, and its record stays`. The `+ /task` +`activity` over one line: `work you send off with /task lands here, and its record stays`. The `+ /task` row at the foot of the task column types `/task ` into your box, which is why the word on its own has an answer worth giving. @@ -1582,7 +1582,7 @@ happened: `/task adaptive` on its own, with no brief after it, starts nothing and prints the usage line instead: `usage: /task · /task solo `. -The `starting a task` row in `/settings` → Session decides what the plain form does: +The `task planning` row in `/settings` → Tasks decides what the plain form does: `sized` is the default and is the behaviour above, and `single` always starts one worker and does not read its width at all. `solo` typed on the command line overrides the row either way. Two of the row's old answers are gone — `ask` with the two-choice list it @@ -1637,12 +1637,12 @@ and nothing to walk between — the arrows keep their ordinary meaning — and w forming there is no head that counts, only the block described under *Why is there a line next to my task*. A typed `/task` never joins the block: it has no wait in front of it. -## /history — task history in the sessions place, including past tasks +## /history — task history in the activity page, including past tasks -`/history` opens the full-screen `[sessions]` place, the same destination as `ctrl+.`, +`/history` opens the full-screen `[activity]` place, the same destination as `ctrl+.`, `alt+4` and a bare `/task`. It lists this machine's conversations and their nested tasks across projects and earlier sessions. The column beside a conversation holds -that conversation's tasks; the sessions place holds the broader record. There is no +that conversation's tasks; the activity page holds the broader record. There is no `/tasks` command. `/task ` and `/task solo ` start work; `/history` only opens the record. @@ -1659,7 +1659,7 @@ otherwise it opens that task's record card. `→` opens the row's available actions. Another window's running task can be opened or located when its row offers that action. -On a machine with no task record, the place still opens under `sessions` with +On a machine with no task record, the place still opens under `activity` with `work you send off with /task lands here, and its record stays`. ## /crew — the crew panel, and the three seats a task runs on @@ -1769,7 +1769,7 @@ this split when it opens; configure the remote profile on that machine. `/settings` (or `/set`, `/config`, or ctrl+,) opens a fullscreen page: a tab bar over the codeaf settings, plus a tab of connected accounts. It was the first of the three fullscreen -pages here — the others are `/history` (the sessions place, ctrl+.) and `/home` — and **only one +pages here — the others are `/history` (the activity page, ctrl+.) and `/home` — and **only one of the three is ever up at a time**: opening any one closes the other two. Moving in it: @@ -1777,7 +1777,7 @@ Moving in it: - ↑ / ctrl+p and ↓ / ctrl+n move a row at a time. Headings are stepped over, never landed on. pgup/pgdown move 16. home/end jump to the ends. - ← and → switch sections, clamping at the ends rather than wrapping. **`tab` no longer - does**: it is the way to the next place — home, teams, sessions, spend, + does**: it is the way to the next place — home, AI teams, activity, memory, spend, settings — here as everywhere else, and `shift+tab` walks that circle back. The panel's own bar is the second one, under the places' bar. - **Any printable key types into a search box** that filters across all tabs at once, @@ -1790,12 +1790,17 @@ Moving in it: - A click on a tab word switches tabs. A click on a row **selects** it, and a second click on the already-selected row **acts** on it. One press never does both. +Choice settings open a list with the current value marked; Enter or a click chooses +and saves. Text and number editors offer adjacent Save/Cancel controls. Invalid +input keeps the draft and shows its error beside it. Escape cancels an open editor +or choice list without saving. + esc backs out one layer at a time: the search first, then anything the `Connections` tab has standing open, then the panel. The head line says `esc close` on the right. The bottom legend normally reads `↑↓ move · ←→ tabs · enter change · type to search · esc close`. -Only the selected row shows its description, at most two wrapped lines. A `•` (or `*` in +Selected and hovered rows share a stable description area with scope and known restart requirements. A `•` (or `*` in ASCII) before a value means you changed it from the untouched default. `set by ` after a value means an environment variable holds it. @@ -1807,11 +1812,12 @@ Every change made in the settings panel goes through the settings registry and i **saved to your global profile**. So does a change you ask codeaf for. There are **two doors onto one file**: the panel, -and the `change_setting` tool codeaf reaches when you say "set my daily budget to 5" or -"use a different model for planning". Both go through the same registry, take the same +and the `change_setting` tool codeaf reaches when you say "hide hints" or +"expand completed tool details". Both go through the same registry, take the same validation, refuse in the same words, and land in the same `config.json` — so a row you change by asking is a row you find changed in the panel, and the reverse. `settings` is -the read beside it, listing every row by the key `change_setting` names. Which rows +the read beside it, listing live-chat rows by the key `change_setting` names. It uses +the same categories and labels as the panel and explains raw write values. Which rows codeaf refuses to change for you, and why, is on the permissions page. The project layer, `/.codeaf/config.json`, is deliberately not writable from @@ -1879,191 +1885,48 @@ config.json keys are not read: models, tiers; anything set under them is ignored It names every unread key, sorted. It is said **once per profile for that set of keys**: the next launch with the same keys says nothing, and a set that changes — a key added -or taken away — is said again. It is not a tip, so turning tips off in the Display -tab's `hints` row does not hide it, and a conversation over `--host` says it about the +or taken away — is said again. It is not a tip, so turning tips off in the General +category's `show hints` row does not hide it, and a conversation over `--host` says it about the profile on the machine running the work. Nothing is rewritten: to act on it, move the value to the key a settings row names (`settings` lists every one), or delete the key. -## The ten settings tabs - -The tabs, in order: - -``` -Session · Context · Workspace · Display · Spending · Safety · Tasks · Teams · Providers · Connections -``` - -## The settings tab strip on a narrow terminal - -On a terminal too narrow for all ten tabs, the strip **scrolls** rather -than being cut: the tab you are standing on is always drawn and always inked, its -neighbours are drawn while they fit, and each end that had to give a tab up wears a `…` -saying there is more that way. `←` and `→` still walk the tabs one at a time, and the -strip follows. +## Settings categories — where did Session, Context, Workspace, Display and Providers go -It anchors left while you are near `Session` and right while you are near `Connections`, -so both ends of the walk look exactly like a strip that fits. A tab the strip could not -draw cannot be clicked, because nothing on this surface acts on something you cannot see. +Settings now groups controls by what you want to change: -**Session** — the rows this conversation carries that belong nowhere else: "memory" and -"fallback models". Two rows, and that is the honest size of it. +`General · Models · Memory · Tasks · AI teams · Permissions · Spending · Connections · Privacy` -The four ssh rows a `--host` conversation rides on ("ssh reuse", "ssh heartbeat", "ssh -missed heartbeats", "ssh traffic") are **not** here any more — they are on **Workspace**. -Every one of them lands on the next launch rather than on the conversation in front of -you, so they are a fact about this machine and not about this session. Nothing you saved -moved: the keys they are stored under are unchanged. +General holds appearance, input and updates. Models holds model selection and +advanced routing/context controls. Memory holds remembering across chats and a +link to saved memories. Tasks controls delegated work; AI teams sets defaults +for groups of AI chats. Permissions controls approvals. Spending holds limits. +Connections holds accounts, keys, web search and remote connection settings. +Privacy holds history, drafts and data-sharing choices. -The models codeaf uses on your behalf are **not** here — they are on Providers, with -the row that says which model you are talking to. They used to be on this tab, one tab away -from it, which made "which model does the planning" and "which model am I talking to" two -errands on two screens. Neither is the conversation's own money limit here any more: it is -`per conversation` on **Spending**. +Old saved keys and values are unchanged. Search accepts old labels and keys, +including settings inside Advanced. Resident-only practice, arrival-brief and +tenure controls no longer appear in chat settings. See **Settings** for scope, +activation timing and how an existing profile carries forward. -**Context** — what a model carries. Rows: "compact at", "answer room", "working set", -"context reuse", "searching", "exa key", "firecrawl key", "jina key". +## Settings categories on a narrow terminal -The selected **searching** row is a live explanation rather than a static -description. On untouched `auto` it reads `now firecrawl, keyless — set -search.exaKey or search.firecrawlKey to raise it`; a pinned Exa row with no key -quotes the exact `Search failed (exa): no API key` answer; a configured pin -reads `exa, with your key`. Provider and key changes land on the next search in -the conversation already open. +Wide terminals show a category list beside the controls. Narrow terminals show +a scrolling category bar. The selected category remains visible; `…` indicates +more categories. Left and right change category and the bar follows. Only +visible controls can be clicked. Hover shows row help without changing its value. -**Workspace** — this machine and this project: what codeaf does with its own time here, and -what it may reach on your behalf. Rows: "quiet before practice", "arrival brief after", -"tenure after", "background checks", "model in commits", "google sign-in id", "google sign-in -secret", "slack sign-in id", and the four ssh rows — "ssh reuse", "ssh heartbeat", "ssh -missed heartbeats", "ssh traffic". **It holds no money row at all** — every one of those -moved to Spending. - -The ssh rows are here because "what may codeaf reach on your behalf" is this tab's own -question, and a link to another machine is that question asked about a machine rather -than about an account. They are not on the tab named **Connections**: that one is the -catalog of third-party accounts you sign in to, and it is built from the account list -rather than from the settings registry. - -**"background checks"** is on by default: one small timer under your own login checks -your reminders, watches and routines every 5 minutes with no window open. Off removes it -and nothing standing is lost — see *Keeping an eye on things* for the whole of it. - -**Spending** — money, and nothing that is not money. Seven lines: the reading `today`, then -"per day", "per conversation", "per plan", the readings "per task" and "per standing run", -and "practice". `/budget` and `/limits` open it. *Models, context, and what it costs* has -every row and every door onto them. - -**Safety** — what codeaf may do without asking you first. Rows: "ask before running", -"tool exceptions", "shell command rules", "guardian", "approval countdown", "background -after", "task countdown", "who settles work that needs a look". - -**Tasks** — how work you can walk away from is run. Rows: "starting a task", "check task -work", "task repair rounds", "tasks at once", "busy machine", "memory floor", "task -model". +Type to search across settings and connection services. Escape clears the +search before leaving settings. Advanced controls remain searchable while +collapsed. Escape from an editor cancels its unfinished edit. ## What the number on a settings row is counted in -Every row that shows a number shows what the number counts, so a whole tab can be read -without moving the cursor onto each row. The unit is part of the value: - -| Row | Reads | Counted in | -| --- | --- | --- | -| ssh reuse | `300s` | seconds a connection stays reusable after its channel closes | -| ssh heartbeat | `3s` | seconds of silence before ssh asks whether the far machine is there | -| ssh missed heartbeats | `3` | unanswered heartbeats — the label says what is counted | -| approval countdown | `10s` | seconds an approval question counts down before it pauses and keeps waiting | -| background after | `30s` | seconds a foreground command runs before it becomes a job | -| task countdown | `15s` | seconds a proposed task waits for you before it starts | -| compact at | `60%` | how much of the model's window is filled before compaction | -| answer room | `65536 tok` | tokens every call keeps free for its answer | -| working set | `160000 tok` | tokens of material kept quoted in front of a worker | -| context reuse | `250%` | shares of one whole context a job may re-send — 100% is once | -| memory floor | `1536 MB` | megabytes that must be free before another task starts | -| busy machine | `1.5 per core` | the load average per core at which new tasks wait | -| tenure after | `3 clean firings` | clean firings a standing charter needs to earn tenure | -| chat width | `50%` | the chat pane's share of the frame while the task rail is open | - -A row whose label already names what is counted — "task repair rounds", "tasks at once", -"ssh missed heartbeats" — shows the figure alone rather than saying the word twice. - -**A row takes its unit back.** The box a value is typed into opens on the bare figure, and -typing the unit back in is accepted: `120s` on "ssh reuse" and `120` both save 120 -seconds. Anything else is refused in the row's own words. - -**A duration row is different and always was.** "quiet before practice" and "arrival brief -after" are written the way you would say them — `20m`, `4h`, `1h30m` — and `0` turns them -off. - -**Display** — how the surface draws itself and what it remembers of your typing. Rows: -"input history", "keep drafts", "task column", "chat width", -"mouse", "timestamps", "turn work". There is no "nerd font" or "linear mode" row: icons -need no patched font anywhere on this surface, and the accessible single-column rendering -is the `--linear` flag at launch rather than a persisted setting. - -**Providers** — which model answers what. It leads with the **Models section**, in this -order: - -1. **your model** — the model you are talking to. It is the conversation slot, and picking - here is the same road `/model` takes — the same picker, providers and all. Its value carries - the provider serving it: `deepseek/deepseek-v4-flash · auto (cloudflare now)`. -2. **provider** — which provider answers your model. enter opens them with - what has been measured of each, and enter on one pins it. -3. **speed guard** — whether an answer slow to start is asked of the next-best provider as - well. -4. **routing** — what every request prefers among the providers, and it cycles - `simple`, `latency`, `price`, `off`. **`simple` is what it ships as**: codeaf sends no - preference of its own, a provider you pinned goes out as the whole request, and with no pin - the router's own default routing answers. `latency` asks for the fastest provider and - `price` for the cheapest, on every call. With `off` nothing is measured, so the two rows - above it have no provider to name. -5. **prompt profile** — how much codeaf tells the model before you type: `auto`, `lean`, - `full`. Another cycle row. `auto` reads the model's context window and goes lean under - 32,000 tokens (see *Models, context, and what it costs*). -6. **reflex** — `near-free · reads every turn — memory, titles, safety` -7. **small work** — `cheap · the small calls — names, digests, the safety gate` -8. **seats** — `the worker, planner and checker, the models they may be picked from, the providers they may route through, and the per-task and daily caps · enter opens /crew` -9. **pinned roles**, and hanging off it the **roles** list — one row per auxiliary call - codeaf makes for itself, grouped under its row. Those rows come from the running binary - rather than the settings registry. - -**worker**, **checker** and **planner** are the crew's three seats, and on the tab they are -one row, **seats** (`auto · 1 pinned · models open · 3 of 4 providers · per task $5 · crew daily cap $5.00`). -`enter` on it opens the `/crew` panel, where the seats, the allowed models, the providers -and the per-task and daily caps are changed; `esc` there comes back to the row. - -**A pin for a role this build no longer has is ignored, and the row stops showing it.** Roles -come and go with the calls that use them — `compaction` was one, and a compaction has not asked -a model since long before it was deleted. A pin left behind for a word like that is dropped -when the row is read, never written back, and the foot line says `compaction is no longer a -role — that pin is ignored` the next time you change any pin. **Every other pin on the row -keeps working**, which is the whole point: the row is one string holding all of them, and -refusing the lot over one dead word would leave you unable to change any of them without -editing `config.json` by hand. - -Typing a word that is not a role is still refused outright, with the real names listed — that -refusal is for the pin you are adding now, which is the one you can do something about. - -The four machine rows lead because the endpoint serving your model is part of the same -decision as the model, and they used to sit at the foot of the tab, forty rows below it. - -Then the rest of the tab: "looking", "reading", "reply guard" — whether a reply -that has come apart is cut and asked again, on by default (see *Models, context, and what -it costs*) — and one row per capability slot added automatically from the settings -registry: drawing, speaking, composing, filming, voice. - -The reflex and small work rows are **select** rows and open the model picker. The crew's -worker, checker and planner have no rows of their own here: the one **seats** row opens the -`/crew` panel, and a seat is pinned there or with `/crew pin` — a pin may carry a thinking -level, `/crew pin planner moonshotai/kimi-k3:high`, and the seat is then asked at that level. - -The connected model providers have their own section on the tab, each with its billing -door, the safe spelling of its key, its region and its order. The section ends with a -`+ add a provider` row, and once a custom provider is connected an `active -provider` row follows it: it reads -`answering on localhost · enter moves it to homelab`, and enter moves this conversation -onto the next provider, wrapping past the last back to the first. The -[services page](services.md) has the whole of it. - -**Connections** — the accounts this profile has connected and what each may do. Its rows -come from the engine rather than the settings registry. +Settings show units with their values: dollars, seconds, percentages, tokens, +RAM in MB and task counts. The editor explains accepted values and validates +before saving. Zero is not interchangeable between controls: it can mean no +limit, waiting indefinitely or disabling a behavior. Read the selected control's +help before changing it. Invalid input keeps the editor open with an explanation; +it does not silently save another value. ## Changing a row in settings @@ -2092,7 +1955,7 @@ hear, and everything else follows the general chat rule. Its legend is Because the picker is the same component, everything true of `/model`'s ranking, its rows and its ctrl+t effort knob is true here too — **including the providers** on the row that has -them. On **your model**, `→` or `tab` unfolds the providers serving the model under the +them. On **chat model**, `→` or `tab` unfolds the providers serving the model under the cursor, walks the cursor into them, and `enter` on one pins it, exactly as under `/model`. The legend says `↑↓ move · → or tab providers · enter choose · esc cancel · type to filter` on a model and `↑↓ move · ← or tab back · enter choose · esc cancel · type to filter` @@ -2103,9 +1966,15 @@ key. While any of those layers is up — the value box, the model picker, the key box on the Connections tab — the foot drops `tab next place`, because the layer has taken that key. +## Task model seats and their limits in Settings + +Models → Advanced includes the seats door to `/crew`. Its default summary is +`per task $5 · crew daily cap $5.00`; actual saved limits appear when configured. +Change those limits in `/crew`, which owns them, rather than a duplicate setting. + ## The roles rows in settings — pinning a role, and del to unpin -The **roles** list sits on the **Providers** tab, directly under "pinned roles". Each row is +The **roles** list is in **Models → Advanced**, alongside "pinned roles". Each row is one call codeaf makes outside a turn — `title`, `guardian`, `checker`, `planner`, `designer`, `worker`, `router`, `vision`, `reflex`, and `spellout`, which is the one of them you ask for yourself with `ctrl+r` (see the keys page) — drawn as @@ -2136,7 +2005,7 @@ in the models page. ## Which model draws my pictures, speaks, films, or looks at an image -Each of those is one row on the **Providers** tab, and the row is the front door: the model +Each of those is one row on the **Models** category, and the row is the front door: the model you pick there is the model that runs. A blank row reads `automatic`, which is not "off" — it means codeaf picks one for you. diff --git a/internal/manual/chat/conversations-and-teams.md b/internal/manual/chat/conversations-and-teams.md index fd76730bfb..857843b33d 100644 --- a/internal/manual/chat/conversations-and-teams.md +++ b/internal/manual/chat/conversations-and-teams.md @@ -138,10 +138,10 @@ to several teams. The Chats overlay shows all the selected team's members, manag the `▦ All` grid always shows all saved conversations regardless of team. Create a team with **+ New team** below the Teams sidebar list. **+ Add subteam** beside -Add member creates one under the selected team. A dedicated dialog over Teams offers Name, +Add chat creates one under the selected team. A dedicated dialog over Teams offers Name, Colour, and an optional searchable Name/Project member list. Click several rows to select them; filters keep earlier selections. Leave the list unselected to create an empty team and use -Add member afterward. `tab` changes fields, arrows move through members or colours, `space` +Add chat afterward. `tab` changes fields, arrows move through members or colours, `space` selects the highlighted member, `enter` creates, and `esc` cancels. Manager assignment stays separate. Duplicate team names are refused; Rename is in Settings. @@ -157,10 +157,10 @@ its members. `esc` cancels naming, then selection, then the grid. Cancelling doe memberships or the conversation's draft. Both creation routes use the same saved catalog and creation rules. -Use **+ Add member** in the team's overview to add an existing conversation or create one. +Use **+ Add chat** in the team's overview to add an existing conversation or create one. Use the member card's **x** to remove that membership with confirmation. Current work finishes, and the conversation survives with its other memberships. Assign another manager before -removing the current one. **Choose manager** selects an existing member; to use a new +removing the current one. **Choose AI manager** selects an existing member; to use a new conversation, add it first, then choose it. A manager may manage its team's descendants but cannot also manage an unrelated team or be both the global and an ordinary team's manager. @@ -238,7 +238,7 @@ it does not select an overlay. ## A team's manager A **manager** is a conversation that runs the team: you talk to it, it sends work to members -and reports progress. Teams offers **Choose manager** for existing members and **+ Manager** +and reports progress. Teams offers **Choose AI manager** for existing members and **+ Manager** when a team has none. Add a new member first when you need a new conversation to replace an existing manager. All teams has a dedicated Global manager card. `+ Global manager` creates its optional conversation. Its reports are the managers of open top-level teams; subteam managers @@ -246,7 +246,7 @@ report to their parent. Its card's `x` deletes the global-manager conversation w normal confirmation, preserving every team and its manager. The creation control returns. In a selected overlay the manager's tab is pinned at the left. It reads `◆ Manager`, or -`+ Manager` while there is none. The grid does not pin managers or relabel their tiles. +`+ AI manager` while there is none. The grid does not pin managers or relabel their tiles. `alt+m` goes to the selected team's manager. A manager can manage descendants of its team, but cannot manage two unrelated teams. Ordinary memberships in multiple teams are allowed. diff --git a/internal/manual/chat/delegates.md b/internal/manual/chat/delegates.md index d2a723512e..ba3041488e 100644 --- a/internal/manual/chat/delegates.md +++ b/internal/manual/chat/delegates.md @@ -32,7 +32,7 @@ did. ## Where a program's badge appears — and where it does not **Every program's tasks wear its name as a badge**: `[]` after the task's title on -the side list, the proposal card, the task's page, the `@` list, the sessions place and home, and its +the side list, the proposal card, the task's page, the `@` list, the activity page and home, and its initials (`[sd]` for senior-dev, `[s]` for sec, `[r]` for review) where a list is narrow. A task codeaf's own worker does wears none, and a program added to codeaf later gets its own badge from its name. The landed card's head shows the title and outcome without a program badge. diff --git a/internal/manual/chat/getting-started.md b/internal/manual/chat/getting-started.md index e81f85419b..ef19da62fd 100644 --- a/internal/manual/chat/getting-started.md +++ b/internal/manual/chat/getting-started.md @@ -109,7 +109,7 @@ leaves this line under the box and stays on the step: What it writes for the default provider: the `api_key` field of your profile's `config.json` (under `~/.codeaf`), owner-readable only. That is the same field the **openrouter key** row on the settings -panel's Providers tab writes, and the one every later launch reads. The running +panel's Connections category writes, and the one every later launch reads. The running conversation takes it at once — the next message rides it, no restart. ## Skip the default OpenRouter provider, retry later, and keep the message I typed @@ -337,7 +337,7 @@ Every answer went through a settings row, so every answer has a door: | What you answered | Where to change it later | | --- | --- | -| the default provider's openrouter key | clear or remove it and the next local interactive launch offers **connect openrouter** again; `/settings`, Providers tab, the **openrouter key** row still accepts a pasted replacement | +| the default provider's openrouter key | clear or remove it and the next local interactive launch offers **connect openrouter** again; `/settings`, Connections category, the **openrouter key** row still accepts a pasted replacement | | the crew | nothing was asked — it is auto. `/crew` shows it, and `/crew pin ` pins a seat | | the daily limit | `/budget` (also `/limits`), or `/settings` → **Spending**. `CODEAF_DAILY_BUDGET` in your shell outranks the row | | the model you talk to | `/model`, or the **Chat model** row on the setup screen — the same settings row either way | diff --git a/internal/manual/chat/hints-and-tips.md b/internal/manual/chat/hints-and-tips.md index 9bb658491d..efdea84f40 100644 --- a/internal/manual/chat/hints-and-tips.md +++ b/internal/manual/chat/hints-and-tips.md @@ -233,7 +233,7 @@ of the commands they named still works** — only the tips about them are gone. - `/attach lets you browse anywhere for files` was a second row about one command, which is one row too many. - `ctrl+. sees every task this project has run` came off on the owner's word, the last of - the three reads. The chord still opens the sessions place, `/history` still opens it too, and + the three reads. The chord still opens the activity page, `/history` still opens it too, and the *tasks* manual page still says so. Unless a line above says otherwise, a tip is true from the first minute on home and after @@ -247,17 +247,9 @@ for a chord that works only in a conversation and only over a making-shaped sent ## Turn off hints — stop showing tips, disable the hints, the disable hints row -Open the settings panel with `/settings` (or `ctrl+,`), go to the **Workspace** tab, and flip -the **disable hints** row on. The line under the row reads -`disable💡 tips everywhere (requires restart)`. Enter or space toggles it; it is off by -default, which means the tips show. (Until 2026-09-22 it was a **hints** row on the Display -tab, on by default.) Inside a running codeaf the change lands at the end of the next turn, -in every conversation and on home alike; restarting codeaf is the only way to have it at once. On silences the tips — over a conversation's -box and over home's alike — and the what's-new lines together; it does not touch the keys -row's own words for a live state — `esc interrupt` and the rest are not hints and cannot -be turned off. +Open `/settings` (or `ctrl+,`), choose **General**, and turn **show hints** off. This hides contextual tips and what's-new notices; the keys row still explains available actions. Turn it on to show tips again. Tips already retired stay retired. -Turning the row back off shows whatever is due. Tips you had already retired stay retired. +Existing profiles retain their preference. If you previously turned **disable hints** on, **show hints** now appears off. ## What "news" lines are — what's new after an update @@ -269,4 +261,4 @@ of the same build says nothing. There is nothing to announce yet, so no news line has ever been printed by this build. A first launch on a fresh profile says nothing either — nothing is new to somebody who never saw -the older build. The **disable hints** row on the Workspace tab silences news lines along with the tips. +the older build. Turning **show hints** off under General silences news lines along with tips. diff --git a/internal/manual/chat/home.md b/internal/manual/chat/home.md index c91f6eba7e..c2f2f4714f 100644 --- a/internal/manual/chat/home.md +++ b/internal/manual/chat/home.md @@ -59,7 +59,7 @@ There is no argument form. The screen is how you name what you want; a command t project name would be asking you to type out the very thing home exists to show you. Home is the **first place on the top line**, right after the `codeaf` wordmark: -`home teams chats sessions spend settings`. +`home chats AI teams activity memory spend settings`. The **teams page**, right after it, is where your teams and their managers live. It still does nothing on its own: no notifications and no alerts. You open it, you see where things stand, and you either act on something or leave. @@ -188,7 +188,7 @@ watch that needs somebody, never age off home. as an ordinary line once it has been answered or has aged out of the group. **On a short frame the whole group folds to one line before any question gives way** — -`8 unread`. `enter` expands that group; the `sessions` heading opens the sessions place. +`8 unread`. `enter` expands that group; the `sessions` heading opens the activity page. ## Answer from home — a digit answers the question that is drawing its answers @@ -245,7 +245,7 @@ A conversation another terminal is holding is not refused: `enter` brings it her Work that landed as `your call` **waits until you decide about it**, however many days that is: nothing more happens to that work until somebody answers it. Only its row on home ages — two days after it landed it leaves `needs you` for the count on the panel's last line, -`3 more`, and the sessions place still lists it; `enter` on that line opens the panel and shows +`3 more`, and the activity page still lists it; `enter` on that line opens the panel and shows it again. Three ways to settle it, and they are the same door: @@ -271,18 +271,18 @@ not where it is settled.** ## Sessions — the fifteen most recent conversations on Home and what is running on this machine right now -The **sessions** section replaces the old list of individual tasks. It shows up to +The **activity** section replaces the old list of individual tasks. It shows up to fifteen conversations across projects, newest conversation activity first, regardless of whether they have tasks. Closed conversations remain in history with dim titles. Each row uses the full conversation title, truncated to fit, and its age. Selecting a row opens that conversation; `→` offers the usual conversation actions. -Click **sessions** to open the **sessions** tab (formerly tasks), which holds the full +Click **activity** to open the **activity** tab (formerly tasks), which holds the full conversation trees under running and completed. The running section shows work in flight across projects. Short frames show fewer rows with a fold for the remainder of these fifteen. The heading leads to the full history. -There is exactly one conversation list on Home, under **sessions**. Open tabs, +There is exactly one conversation list on Home, under **activity**. Open tabs, recently closed conversations and saved history are combined by conversation identity before choosing the fifteen most recent, so each conversation appears once. The `opt+k` chats menu still lists open tabs; dismissing or reopening a tab @@ -291,7 +291,7 @@ updates the tab and the row in Sessions together. ## Why does home show a session id for an untitled chat, untitled conversation, new conversation **A conversation nothing has named yet reads `new conversation`** on home's sessions -list, the same word the sessions place uses. It never reads as its session id, including +list, the same word the activity page uses. It never reads as its session id, including the id with its first letter raised (`D53cceead3f99593`). A conversation that has a title keeps that title. @@ -325,7 +325,7 @@ spinner. In screen-reader (linear) mode nothing turns at all. ## What did it do while I was away — the since you left panel, what happened while the terminal was shut -**What happened on its own while you were not looking**, under `sessions` — in the field +**What happened on its own while you were not looking**, under `activity` — in the field when it holds anything and in the rail when it does not — headed with how long you were away: @@ -423,7 +423,7 @@ no right side at all on the resting screen. Where to go for the whole figure instead: - **`→` then `its chats`** on the row lists every conversation in the project as a search; -- **the sessions place** for the work a project has run, which is the record rather than a +- **the activity page** for the work a project has run, which is the record rather than a count; - **the spend place** for what it has cost. @@ -533,7 +533,7 @@ put would move you onto something else between two glances. **It is not empty; the panels are waiting for their first rows.** On a machine that has done nothing yet, every panel but one keeps its heading and one dim line naming what arrives there — `questions from any chat or task land here · a digit answers them` under -`needs you`, `work you send off with /task runs here on its own` under `sessions`, and so on +`needs you`, `work you send off with /task runs here on its own` under `activity`, and so on (*See everything at once*). `projects` always has the folder this window opened in, and the conversation list has this conversation from its first minute. @@ -590,7 +590,7 @@ panels, and `enter` again folds it. The box at the foot still searches every con the machine as you type — a project's name, a folder's name or a word from what a task came to all find them — whether or not a panel is drawing the row. -Every panel's fold works the same way: `N more` under `sessions`, `since you left`, `standing` +Every panel's fold works the same way: `N more` under `activity`, `since you left`, `standing` and `projects` opens that panel. The places themselves — tasks, standing, spend — are on the tab bar and their slash commands, not behind the folds. @@ -608,7 +608,7 @@ there is no strip. The rule and a blank come next, three rows, and the page star row higher. **Inside a chat** and on every place but home the top line reads: ``` - >● codeaf home teams chats sessions spend settings 2 want you · 4 moving · $0.55 / $500 · tue 1:11pm + >● codeaf home chats AI teams activity memory spend settings 2 want you · 4 moving · $0.55 / $500 · tue 1:11pm ``` **On home it drops the two counts** and keeps the budget and the clock, because the @@ -644,7 +644,7 @@ cent — one reading of one file, wherever you are standing. ## Where did standing and memory go: the six words on the tab bar -The top line reads `home teams chats sessions spend settings`. `alt+1` through +The top line reads `home chats AI teams activity memory spend settings`. `alt+1` through `alt+6` go to each; `chats` (`alt+3`) returns to your conversation. - `/standing` (or `/orders`) and `alt+7` open Standing. @@ -1639,7 +1639,7 @@ reports that it can send one, and there `ctrl+1` … `ctrl+8` are a second spell map's own line names while it is live. Beyond that alias there is no `ctrl+` chord for home: the plain ones are all taken (`ctrl+.` is the -sessions place, `/history`). +activity page, `/history`). ## What landed while I was away — since you left, and the look stamp @@ -1740,7 +1740,7 @@ rows on disk that say `running` forever. Home never repeats that claim. **It asks the session itself.** A live session says out loud, every few seconds, which task nodes it currently has out; the task details show it as running only while the session still names that node. Every other live-looking row is **work that was under way when the window went**, and it is -on no panel: the sessions place and the card beside a search say `incomplete` against it. +on no panel: the activity page and the card beside a search say `incomplete` against it. A session too old to keep that file, but whose journal a window is holding, falls back to the older answer: the lock is asked, and its rows are believed. That is the same rule with @@ -2140,7 +2140,7 @@ Completed work can also appear under `since you left`. ## How do I see more tasks on the right — ▸ …5 more tasks -**On the card beside a search, `▸ N more tasks` names the sessions place** out at the right +**On the card beside a search, `▸ N more tasks` names the activity page** out at the right margin rather than unfolding, because a card is not the place that holds them; `ctrl+.`, or `tab` onto `sessions`, is the way there. A band of files or folders with more behind it says `▸ …3 more files`, and a click on that line — or `→` with the strip closed — opens it, and @@ -2449,7 +2449,7 @@ On the card beside a search and on the phone sheet, a conversation's tasks form their actual parents. Clicking a task opens that task's record. A child needing a person brings its family forward; work still moving comes before settled work. The compact preview keeps three task names in parent-first order, so it never shows a child without its visible -ancestry, and `N more tasks` names the sessions place, which holds the full conversation tree. +ancestry, and `N more tasks` names the activity page, which holds the full conversation tree. ## What home cannot do yet — stop another window's task, and a something is wrong panel diff --git a/internal/manual/chat/keeping-an-eye.md b/internal/manual/chat/keeping-an-eye.md index 4767fd5ce4..af62807679 100644 --- a/internal/manual/chat/keeping-an-eye.md +++ b/internal/manual/chat/keeping-an-eye.md @@ -657,7 +657,7 @@ nothing was lost while there was no key. ## Turn background checks off -`/settings` → **Workspace** → **background checks**, or just say "turn off the +`/settings` → **General** → **background reminders**, or just say "turn off the background checks" and the chat will do it. It is a two-word row — `on` or `off` — and `on` is the default. diff --git a/internal/manual/chat/keys.md b/internal/manual/chat/keys.md index a95d59b842..9ec99a63f1 100644 --- a/internal/manual/chat/keys.md +++ b/internal/manual/chat/keys.md @@ -670,7 +670,7 @@ cannot send it delivers plain enter on many keyboards, or a newline on some. | `ctrl+,` | Open the settings panel | | `alt+e` | Walk this conversation's thinking rung one step: auto → low → medium → high → xhigh → max, and back to auto. Works with a sentence half typed. On home and every other place it walks the rung of the **next** conversation instead — the effort word after the model’s colon on home’s seam | | `alt+a` | Walk what this conversation runs without asking one stop: asks → guardian → YOLO → asks. Never lands on `refuses`. Works with a sentence half typed; over `--host` it says the far machine's rules decide. On home and every other place it walks the gate of the **next** conversation — the `◇` cell on the rule above that box — and that pin is spent by the conversation that uses it | -| `ctrl+.` | Open the sessions place (`/history`) — every task this machine has run, across every project and every session; type to filter it. It opens on a machine that has run nothing too, and the page says what tasks are | +| `ctrl+.` | Open the activity page (`/history`) — every task this machine has run, across every project and every session; type to filter it. It opens on a machine that has run nothing too, and the page says what tasks are | | `space` `space` | On an **empty** box: open home (`/home`) — every project and conversation on the machine the session runs on, and an empty home on a fresh one. Does nothing when the box has words in it | | `ctrl+l` | Jump back to the live edge of the conversation | | `alt+v` (`opt+v`) | Open the **conversations view** (`/wall`) on Chats: all saved, non-deleted conversations, with live or saved previews. Select cards to create a team. Press again or `esc` to close it. Its own keys are on the *Conversations and teams* page | @@ -1369,8 +1369,8 @@ What it changes and what it does not: it. It is sticky, kept in this session's own `meta.json`, so it is still there after you close codeaf and `/resume`. `codeaf resume --yolo` outranks the saved word for that launch. -- It does **not** change other conversations. Their answer is the **"ask before running"** - row and the **guardian** row on `/settings`' Safety tab, unless they have a saved +- It does **not** change other conversations. Their answer is the **tool approvals** + row and the **AI approval screening** row on `/settings`' Permissions category, unless they have a saved posture of their own. - **Neither floor moves.** Dangerous shell commands and anything sent in your name are asked about at every stop, `YOLO` included, exactly as under `--yolo`. @@ -1725,7 +1725,7 @@ follows what you type. Only these keys are taken from you: **Model picker** — opened by `/model` with no argument, by clicking the model name in the status row, or by `enter` on the **your model** row of the settings panel's -Providers tab (the same list and the same keys, drawn in the panel's place): +Models category (the same list and the same keys, drawn in the panel's place): `esc` close · `enter` switch to the highlighted model · `ctrl+t` cycle the reasoning effort · `ctrl+r` fetch the newest model list (`/model` only — not the settings panel's @@ -1757,7 +1757,7 @@ Every model picker column is two rungs — its own direction, then reversed — `model ↑`, `via ↓`, `via ↑`, and so on back round to the name, skipping any column this list published nothing in; `alt+shift+s` retraces it. The list is always sorted and the sorted column always wears `↓` or `↑` in the heading. Inside an open provider fold the same key -sorts the PROVIDERS, and the two tables keep their own orders. It is a chord and not a bare `s` for the reason the sessions place gives: +sorts the PROVIDERS, and the two tables keep their own orders. It is a chord and not a bare `s` for the reason the activity page gives: `s` is one of the commonest letters a filter starts with, and the list a person was narrowing would re-sort instead. @@ -2331,8 +2331,8 @@ to filter, `↑↓` to walk, `enter` to use it, `esc` to go back to the layer. **Press the space bar twice with an empty message box.** That is the way back to home from inside a conversation, and `/home` opens it too. -**There is also a number: `alt+1` (`opt+1` on a Mac).** Home is the first of the six places on -the top line, `home teams chats sessions spend settings`, and each answers to its position there, +**There is also a number: `alt+1` (`opt+1` on a Mac).** Home is the first of the seven main destinations on +the top line, `home chats AI teams activity memory spend settings`, and each answers to its position there, `alt+1` through `alt+6`. **`alt+7` and `alt+8` are kept**, on the two places that are off the bar — standing and memory — so those keys still open a room rather than doing nothing; `alt+.` draws them all with their numbers. `alt+2` is the teams page, and @@ -2365,7 +2365,7 @@ tasks, on standing orders and on memory alike: a foot that offered `enter` or `t filter` over a body with no rows would be naming a key with nothing to act on. There is no `ctrl+` chord for home: every one this surface could use is already -taken, and `ctrl+.` is the sessions place (`/history`) from a conversation — while a place is +taken, and `ctrl+.` is the activity page (`/history`) from a conversation — while a place is standing that same `ctrl+.` draws the map, on the terminals that can send it, because a place takes the whole frame and never reaches the conversation's keys. `esc` was not available either: on an idle conversation it already arms rewind and already clears messages waiting from the turn, and a third @@ -2623,7 +2623,7 @@ column's two words, and elsewhere they do nothing. **The walk stops at this conversation's last job, after its last task.** The roster holds this conversation's work, then the jobs section under it, so `↓` walks both and clamps at the bottom rather than carrying on into the project's record. Old tasks from earlier -sessions are on the sessions place, reached from the column's own `ctrl+. earlier` line, from +sessions are on the activity page, reached from the column's own `ctrl+. earlier` line, from `ctrl+.` or from `/history`; `enter` on an `earlier` row there goes inside that task's card. In a directory whose earlier sessions ran tasks but where **this** conversation has run none and started no jobs, `alt+t` falls through: there is nothing on the column to @@ -2751,7 +2751,7 @@ A program's task page — senior-dev's — opens on the actions it took, each un of its process. **`ctrl+y` turns it to the raw calls** it made to its model: what it sent, what the model answered, which model it was, and the call in flight. `ctrl+y` again turns it back. It works in the program's room, whichever door opened it — its row, its card, or -the sessions place — and the key row names it: `ctrl+y calls` over the actions, `ctrl+y actions` +the activity page — and the key row names it: `ctrl+y calls` over the actions, `ctrl+y actions` over the calls. Every page opens on the actions. It is a chord, so it never costs a character: the room's box keeps what you typed. It is @@ -2820,7 +2820,7 @@ or a part of one it handed out itself: whole of it, and no key on this surface ever does something that is not drawn on the screen in front of you. Where a landing draws each answer on a line of its own — the one road that does, because its `[a]` moves files of yours — `d you decide` is on it. The standing -choice is `task.settle` in `/settings` under Session, and it is the right place for it: a +choice is `task.settle` in `/settings` under Tasks, and it is the right place for it: a letter that hands one landing over changes nothing about the next one. While `task.settle` is `auto` the reason row also reads `codeaf is deciding`; the answers @@ -3104,8 +3104,7 @@ press a key or press `ctrl+s` again. While the pointer is out, the row under the box reads exactly `drag to select · any key ends it`. That line is how you tell this apart from everything else here. -**The `ui.mouse` setting is off.** It is **on** by default — `/settings`, the Display -section, the row labelled `mouse`. With it off, codeaf never asks your terminal to report +**The `ui.mouse` setting is off.** It is **on** by default — `/settings`, the General category, the row labelled `mouse interaction`. With it off, codeaf never asks your terminal to report the pointer at all: no hover, no click, no wheel, and your terminal keeps drag-select permanently. `/select` then says `your terminal already has the pointer — drag to select.` @@ -3397,11 +3396,11 @@ This holds however the row was changed — in the panel, or by asking codeaf to `change_setting`. The row is written straight away and the transcript says so; the screen picks it up when the turn ends. -The **background after** row is different. Its key is +The **background shell commands after** row is different. Its key is `bash.background_after_seconds`, and the session engine and the visible countdown arm from it together at launch. A change lands on the **next session**, not at the end of the current turn. Because this row controls how hard the machine may be worked, -`change_setting` refuses it; open `/settings`, choose the Safety tab, and change the row +`change_setting` refuses it; open `/settings`, choose the Tasks category, and change the row yourself. With `ui.mouse` off, every drag belongs to your terminal permanently, and `ctrl+s` has diff --git a/internal/manual/chat/lanes.md b/internal/manual/chat/lanes.md index 21a060cbb1..0014c32b50 100644 --- a/internal/manual/chat/lanes.md +++ b/internal/manual/chat/lanes.md @@ -334,7 +334,7 @@ What that means, exactly: - **the line stays in the conversation.** It is not one of the dim retry notes the work chip collapses when an answer lands, so it is still on the screen after the turn finishes; -- **your settings row is not touched.** The `host` row on the Providers tab still reads +- **your settings row is not touched.** The `host` row on the Models category still reads `pinned: coreweave`, exactly as you wrote it. What changes is everything that names the host **requests are going to**: the `@coreweave` comes off the model's name, the tail on the `your model` row reads `auto (coreweave cannot serve this model)`, and the fold's @@ -511,7 +511,7 @@ The table above is the whole answer, and **`response.attempts` is the one thing you can turn about it**. It is a multiplier on those times, not a number of requests: `3` means three times as long — four and a half minutes on a turn you are watching instead of ninety seconds — and the default is `1`, which is exactly -the table. Set it on the **Providers** tab of `/settings`, or with +the table. Set it on the **Models** tab of `/settings`, or with `CODEAF_RESPONSE_ATTEMPTS`. ``` @@ -926,7 +926,7 @@ model id is already the whole route. ## How do I stop codeaf choosing the host itself — the simple routing mode, OpenRouter's default routing, and what my pinned host still sends -The `routing` row (`/settings` → **Providers**) has a fourth answer, **`simple`**, +The `routing` row (`/settings` → **Models**) has a fourth answer, **`simple`**, for exactly this. Under it codeaf keeps no opinion of its own about the hosts behind your model, and sends none: diff --git a/internal/manual/chat/making-pictures-audio-and-video.md b/internal/manual/chat/making-pictures-audio-and-video.md index 8356093fbe..5c2ace515e 100644 --- a/internal/manual/chat/making-pictures-audio-and-video.md +++ b/internal/manual/chat/making-pictures-audio-and-video.md @@ -133,7 +133,7 @@ as ever, and the next call without the argument rides the default again — a one-call choice never changes any setting. The result line always names the model that actually generated the file, so you can tell which one made what. -This is the same freedom you have yourself in `/settings` → Providers, handed +This is the same freedom you have yourself in `/settings` → Models, handed to codeaf per call: ask it to "draw this one with gemini" or "try the best image model" and it can, just in time. @@ -185,7 +185,7 @@ expansion shows, e.g. `Image generation failed (bytedance-seed/seedream-5-0-pro) To change which model draws, just say so: "draw this one with gemini", "use the best image model". That is the `model` argument on the tool, described in the section above, and it changes nothing permanently. For a new default, use -`/settings` → Providers. +`/settings` → Models. ## Why does a picture or video look generic, blurry, or like AI slop? @@ -268,7 +268,7 @@ with the path, the file size and the model, e.g. you asked for a particular voice, because a voice the model does not have is a failed generation rather than a near miss. -With no speech model set in `/settings` → Providers, the default is +With no speech model set in `/settings` → Models, the default is `fish-audio/s2.1-pro`, falling back to `fish-audio/s1`, then `hexgrad/kokoro-82m` and then `openai/gpt-4o-mini-tts` on a catalog that does not advertise it. A model you set yourself wins over all of them. @@ -366,7 +366,7 @@ note simply omits both rather than guessing. A render that fails says so the same way: `job 3 failed: video generation timed out (); no video was saved`. Nothing waits for it and nothing polls it. -With no video model set in `/settings` → Providers, the default is +With no video model set in `/settings` → Models, the default is `bytedance/seedance-2.5`, falling back to `bytedance/seedance-2.0-mini` on a catalog that does not advertise it. A model you set yourself wins over both. diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 02e608cb8c..69d55f662b 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -215,7 +215,7 @@ Now you pause and press it once. your query. `↑`/`↓` do not count as editing. `tab` always opens and closes. **This is one list with two doors.** `/model` opens it, and so does the **your model** -row at the top of the Providers tab in `/settings` — the same rows, the same name +row at the top of the Models category in `/settings` — the same rows, the same name search, the same providers under `→`, and `enter` on a provider pins it either way. The media slots on that tab (**drawing**, **speaking**, **looking** and the rest) open the same component over their own models, but they have no provider row behind them, so nothing @@ -537,7 +537,7 @@ whenever one serves the model. The crew is three seats because those are the calls a task spends most of its money on. codeaf also makes smaller calls on your behalf — naming a session, the safety gate, memory -— and those ride two rows of their own in `/settings` → Providers, **reflex** and +— and those ride two rows of their own in `/settings` → Models, **reflex** and **small work**, which ship pointed at near-free models and are not part of the crew. **`/model` is untouched.** It is the model you talk to, and nothing about the crew moves @@ -679,7 +679,7 @@ that seat through OpenRouter even when a direct connection also serves the model the rule would have to break is not a pin. And a pinned model none of your connections can reach is not quietly swapped: the task does not start, and says why. -In `/settings` → Providers the three seats are one row, **seats**, which says how many are +In `/settings` → Models the three seats are one row, **seats**, which says how many are pinned, the allowed rule, how many providers are on, the per-task limit and the daily cap; `enter` on it opens the crew panel, and `esc` there brings you back to the row. @@ -903,7 +903,7 @@ is not touched. A family of ### The roles under each row -Directly under the **pinned roles** row, `/settings` → Providers lists **every registered +Directly under the **pinned roles** row, `/settings` → Models lists **every registered role**, grouped under the row answering it, saying which model comes out: | role | row | what it is | @@ -1101,7 +1101,7 @@ asks for high thinking separately. - Where a level is set, the role rows print it after the id, `kimi-k3:low`, which is the same notation the model picker and `/status` use. -The **planner** row in `/settings` → Providers is a text box because a picker hands back a +The **planner** row in `/settings` → Models is a text box because a picker hands back a bare id, while this row may hold an id with a thinking instruction on it. ## Why is my crew thinking at low — the pin is being ignored, effort=low in the log @@ -1143,7 +1143,7 @@ without any one message reading as three times the price of its neighbours. ## Which model the vision role uses -For `vision`, the **looking** row further down the Providers tab is the front door +For `vision`, the **looking** row further down the Models category is the front door for which model sees, and it wins over this role's class. The `vision` pin is the second rung of that ladder — set the looking row for the ordinary case, and pin the role only when you want a pin that also binds the older surfaces. @@ -1172,11 +1172,11 @@ on the first row of the page instead, above the chips. It is moved, not dropped header sheds first is the goal, which you can still read in the conversation. The nodes under the planner run on the `worker` role, which sits on the crew's **worker** — -a different model, and not on this line. `/settings` → Providers lists both. +a different model, and not on this line. `/settings` → Models lists both. ## Pinning one role to its own model, and unpinning it -In `/settings` → Providers, move onto any row of the roles list and press **enter**. That +In `/settings` → Models, move onto any row of the roles list and press **enter**. That opens the model picker — the same one `/model` opens, same filter box, same ranking — and what you choose is **pinned** to that role alone. The row then reads ` pinned`, and the legend at the foot offers **del unpin**. Press @@ -1193,9 +1193,9 @@ and pinning from the list rewrites the row without disturbing the other pins in **A third door: just ask.** "Use `deepseek/deepseek-v4-pro` for planning and for designing harnesses" is a sentence codeaf acts on — it looks the row up with `settings` and writes it with `change_setting`, into the same `models.roles` row, after asking you. The reflex and -small work rows of the Providers tab, and the crew's worker, checker and planner pins behind +small work rows of the Models category, and the crew's worker, checker and planner pins behind its one **seats** row, are writable that way too — a pin outside your allowed models is -refused there as it is on `/crew`; only the role **slots** further down the Providers tab are not, +refused there as it is on `/crew`; only the role **slots** further down the Models category are not, because those are bindings the running session holds rather than values in your profile. So the ladder for any role, most specific first: **its pin**, then **its row's model**, @@ -1605,7 +1605,7 @@ number of tries (see *How long codeaf keeps trying*). **Sometimes it moves after two attempts instead of three.** Three attempts are worth making only when they can reach *different* endpoints. If the stream died before naming -which endpoint served it, or you have set `routing` to `off` or `simple` on the **Providers** tab, then +which endpoint served it, or you have set `routing` to `off` or `simple` on the **Models** category, then nothing is being routed around and the next attempt lands in exactly the same place — so codeaf stops asking and moves to the next model a try earlier. Setting `routing` to `off` or `simple` switches off **endpoint** steering; it does not switch off moving to another model. @@ -2174,7 +2174,7 @@ came back with nothing in it at all. Its row in the provider fold then reads `ba an hour, and every usable answer it serves afterwards walks it back up. Recovery is by serving properly, which is the only evidence there could be. -**Turning it off.** The row is `reply guard` on the **Providers** tab of `/settings`, `on` +**Turning it off.** The row is `reply guard` on the **Models** category of `/settings`, `on` or `off`, and the default is **on**. Off means you see whatever arrives, and keep whatever you stop. You can also just ask codeaf to turn it off; it is not one of the rows it refuses. The two clocks in the @@ -2205,7 +2205,7 @@ reply arrives (*The model was printing garbage* above), so only the text that ha stopped being language is dropped — and the line above is the only time you are told, which is how you can tell the two apart. -**It does not happen at all with the guard off.** `reply guard` on the **Providers** tab of +**It does not happen at all with the guard off.** `reply guard` on the **Models** category of `/settings` is `on` by default; set it to `off` and you see, and keep, whatever arrives — including whatever you stop. @@ -2244,7 +2244,7 @@ what a tool call looks like gets you an honest answer full of exactly this synta that merely names a tool is safe: the markup has to be the substance of the reply, not a word in a sentence. And a conversation with no tools available cannot trigger it at all. -**The switch is the same one.** `reply guard` on the **Providers** tab of `/settings` +**The switch is the same one.** `reply guard` on the **Models** category of `/settings` turns this off together with the repetition guard above; off means you see whatever arrives. @@ -2600,7 +2600,7 @@ name on the row. A thing neither of them knows keeps its id. **`enter` on any row under "by topic" or "by standing order" opens the thing itself**, and the foot says so on every row that is a door — `enter opens what spent it`. The headings said it too for a while; the heading over the cut is the control that swaps cuts now, and -a control with an unrelated instruction after it is two objects on one line. A task opens **its own record card** in the sessions place, with the list +a control with an unrelated instruction after it is two objects on one line. A task opens **its own record card** in the activity page, with the list behind it parked on that row; a standing promise opens the standing place **on that order**; and a conversation **opens** — brought forward if this terminal already has it, otherwise opened beside the one you are in, with all of that door's refusals (a folder that @@ -2647,7 +2647,7 @@ Time is two questions, so it gets two arrow axes and no letters: The cut of the ledger is its own control and has nothing to do with time: walk onto the heading and `←`/`→` swap `by topic` for `by model`. The window opens on **the last 14 days, by the day**. The label between the arrows is the -reading and the control at once, and the same head row is drawn on the sessions place and the +reading and the control at once, and the same head row is drawn on the activity page and the standing place. A terminal too narrow to draw the control has no window there at all — the keys do nothing rather than moving something nothing on screen reports — and the zoom keys are bound only where `shift+↑ coarser` fits beside the arrows. A week buckets from Monday; there is no year rung, because a @@ -2970,9 +2970,8 @@ They live on **one tab**: `/settings` → **Spending**, which `/budget` opens di | **per plan** | `asks first above $100` | a planned job estimated above it quotes its step count and its price and waits for your go-ahead — it asks, it does not stop | | **per task** | `$5 a task` | an ordinary task's next priced call is not made; set in `/crew`. senior-dev has its own run ceiling | | **per standing run** | `$5 a firing` | that one firing stops there; each order may name its own | -| **practice** | `$50 of the day` | codeaf's practice on itself stops until tomorrow, and your own work is untouched | -Above those six the tab leads with **`today`**, which is a reading and not a setting: +Above those limits the category leads with **`today`**, which is a reading and not a setting: `$3.42 of $500 · resets at midnight`, or `$3.42 · no limit` on a machine with no daily limit. Before the first model call of the day it is **not on the tab at all** — a machine that has not spent anything has not spent zero. @@ -2989,24 +2988,21 @@ the conversation itself had spent, so the tab and the row a person pressed to ge disagreed while a task was running. It also counts a call whose receipt arrived after its turn ended, the same moment `today` does. -**Four of the six are rows you can edit** — `per day`, `per conversation`, `per plan`, -`practice`. `per task` and `per standing run` are **readings**: they are real rails, and +**Three are rows you can edit** — daily spending, per-chat spending and the plan approval threshold. `per task` and `per standing run` are **readings**: they are real rails, and neither is a number a settings row could hold. The sections below say why. -The rows used to be spread across two other tabs — the money on **Workspace**, the -conversation's own ceiling on **Session**. They are all on **Spending** now, and -**Workspace holds no money row at all**. +Open **Spending** for monetary limits. Resident practice settings are absent from chat settings because they do not control this chat surface. The separate resident’s practice default is `$50 of the day`; it is not a chat setting. ## Where are the spending limits — the Spending tab, and every door onto it The settings panel's tab bar reads, in order: ``` -Session · Context · Workspace · Display · Spending · Safety · Tasks · Teams · Providers · Connections +General · Models · Memory · Tasks · AI teams · Permissions · Spending · Connections · Privacy ``` Money is on **Spending** and nowhere else. The rows that used to share it are on the two -tabs beside it: **Safety** is what codeaf may do without asking you first (ask before +tabs beside it: **Permissions** is what codeaf may do without asking you first (ask before running, tool exceptions, shell command rules, guardian, approval countdown, task countdown, who settles work that needs a look), and **Tasks** is how work you can walk away from is run (starting a task, check task work, task repair rounds, tasks at once, @@ -3196,7 +3192,7 @@ nothing and says, for a $5 limit, One model id is served by many providers, and they differ in two ways at once: how fast they answer, and what they charge. The published list price beside a model is the model's own figure — no provider is obliged to match it, and the fastest one often does not. -**Left alone, codeaf asks for nothing.** The **routing** row on the Providers tab ships as `simple`, and `simple` means the request carries no preference of codeaf's own: with no provider pinned there is no `provider` object on it at all, and OpenRouter's own default routing picks the provider. Pin a provider and that pin is the whole request — that provider, `only`, no fallbacks, and nothing else added to it. Nothing is ranked, nothing is capped, nothing is retired behind your back, and what the picker shows, what is chosen and what the record says are the same thing. +**Left alone, codeaf asks for nothing.** The **routing** row on the Models category ships as `simple`, and `simple` means the request carries no preference of codeaf's own: with no provider pinned there is no `provider` object on it at all, and OpenRouter's own default routing picks the provider. Pin a provider and that pin is the whole request — that provider, `only`, no fallbacks, and nothing else added to it. Nothing is ranked, nothing is capped, nothing is retired behind your back, and what the picker shows, what is chosen and what the record says are the same thing. It has not always been this way: until this build the shipped row was `latency`, and codeaf asked for the fastest provider on your own turns and the cheapest on work you were not waiting on. That choosing was invisible — the one decision in a turn you could not see being made — so it is now something you turn on rather than something you turn off. @@ -3258,7 +3254,7 @@ model you picked. codeaf calls one of those a **provider** — older builds called it a *lane*, and the setting on disk still does — and you can see them and choose one. -**Three rows on the Providers tab of `/settings` sit directly under **your model**, in +**Three rows on the Models category of `/settings` sit directly under **your model**, in that order** — **provider**, **speed guard**, **routing** — because the provider that serves your model is part of the same decision as the model: @@ -3592,7 +3588,7 @@ is kept, so a rescue is also a free measurement. ## Speed guard — what it costs and when to turn it off -**speed guard** is a row on the Providers tab of the settings panel, directly under +**speed guard** is a row on the Models category of the settings panel, directly under **provider** and two rows under your model, and it is **on**. It hedges **at most one extra call** per answer and stays under **a tenth** of what the @@ -3665,14 +3661,14 @@ probes. That file is the record of what was sent. **How to make it zero.** It is already zero on a home where nobody has touched **routing**: the shipped row is `simple` and it buys none of these. If you have set -`latency` or `price` and want it back to zero: Settings → Providers → **speed guard**, +`latency` or `price` and want it back to zero: Settings → Models → **speed guard**, off. The same row governs asking a second provider when an answer is slow to start, so turning it off stops both. `routing simple`, `routing off`, and a provider row set to `openrouter`, also stop it. ## The provider row in settings — auto, pinned, pinned but borrowable, openrouter -Settings → Providers has two rows under **routing**: +Settings → Models has two rows under **routing**: ``` your model deepseek-v4-flash @@ -3710,7 +3706,7 @@ row drops its `auto (cloudflare now)` tail rather than name a provider nobody ch Every request in a conversation re-sends the whole conversation. What keeps that from costing a fortune is the **prompt cache**: the provider that answered you a moment ago still has those tokens, and re-reading them costs a fraction of sending them fresh. The catch is that the cache sits on **one provider**. An provider that has never seen your conversation charges full price for all of it — measured on a real run, the same 94,000-token context cost **4.7 times more** on a cold provider than on the warm one, and that alone is where a quarter of the requests in that run ate half its money. -**This is something codeaf does under `routing: latency` and `routing: price`, and not under the shipped `simple` row.** Under `simple` the request carries no preference of codeaf's own at all, and asking for last time's provider is a preference — so keeping the cache warm is the router's business there, as the rest of the choosing is. The row is one word away if you want it: `/settings` → Providers → **routing**. +**This is something codeaf does under `routing: latency` and `routing: price`, and not under the shipped `simple` row.** Under `simple` the request carries no preference of codeaf's own at all, and asking for last time's provider is a preference — so keeping the cache warm is the router's business there, as the rest of the choosing is. The row is one word away if you want it: `/settings` → Models → **routing**. Under those two rows, codeaf remembers which provider answered your last request and **asks for that same provider first on the next one**. It is a preference, not a demand: if that provider is busy or gone, the request still goes through somewhere else rather than failing. Nothing extra is sent and nothing is probed to work this out — it is the name that came back on the last answer. diff --git a/internal/manual/chat/permissions.md b/internal/manual/chat/permissions.md index 727bc07c52..4330afd80a 100644 --- a/internal/manual/chat/permissions.md +++ b/internal/manual/chat/permissions.md @@ -298,7 +298,7 @@ chip for as long as anything is open: ``` `alt+y` raises the newest open question from **any page** — home, a room, the -sessions place — and takes you back to the conversation it belongs to. The count +activity page — and takes you back to the conversation it belongs to. The count includes the ones you folded: `esc` is later and not cancelled, so a question you put off is still a question the work is waiting on. @@ -553,7 +553,7 @@ wildcard and all — the dialect has no escape for `*`. ## The approval modes, and the default — does changing "ask before running" affect the conversation I am in `tools.approvalMode` decides what happens when the model asks to run a tool and -nothing more specific applies. It is in `/settings`, under **Safety**, on the row +nothing more specific applies. It is in `/settings`, under **Permissions**, on the row labelled **"ask before running"**. | value | what it means | @@ -700,7 +700,7 @@ legend above the message box names it after the thinking rung — `◇ asks`, The change is **live** — the next tool call is decided under it — and **sticky**: it is written into this session's `meta.json` and is still in force after `/resume`. It changes **this conversation only**; every other conversation -follows the "ask before running" and guardian rows on `/settings`' Safety tab. +follows the "ask before running" and guardian rows on `/settings`' Permissions category. What each posture is, in the gate's terms: @@ -898,7 +898,7 @@ The whole list, by settings key: work costs. - **How hard this machine may be worked** — `task.parallel`, `task.max_load`, `task.min_free_mb`, `bash.background_after_seconds`. The last is labelled - **background after** on `/settings`' Safety tab and arms both the engine's handoff + **background after** on `/settings`' Permissions category and arms both the engine's handoff clock and the countdown for the next session. - **Whether codeaf's own work is checked** — `task.audit`. A session that can switch off the check can call anything done. @@ -925,7 +925,7 @@ conversation's own model (`model.talk`) is changed with `/model`, and the other role slots — `model.plan`, `model.work`, `model.verify`, `model.scribe` — are bindings the running session holds rather than values in your profile, so neither `change_setting` nor the panel can write them. To send codeaf's own -auxiliary calls somewhere, set the reflex or small work row on the Providers tab, pin a +auxiliary calls somewhere, set the reflex or small work row on the Models category, pin a crew seat — worker, checker or planner — with `/crew pin` or on the `/crew` panel that the tab's one **seats** row opens, or pin one role in `models.roles`. @@ -1055,7 +1055,7 @@ Delete the key from your profile's `config.json`, or unset the variable, and the note stops. **What you can still turn off is the model's name.** The **model in commits** -row (`attribution.model`, on the Workspace tab of `/settings`), or +row (`attribution.model`, on the General category of `/settings`), or `CODEAF_ATTRIBUTION_MODEL=0`, leaves `Assisted-by: CodeAF` with no brackets. A landing on the worker harness, the default belt, carries that bare line either way. codeaf cannot change the row for you — ask it to and it says so and points you diff --git a/internal/manual/chat/places.md b/internal/manual/chat/places.md index f202986ace..35ba2f4c60 100644 --- a/internal/manual/chat/places.md +++ b/internal/manual/chat/places.md @@ -2,25 +2,17 @@ ## What a place is, and the seven of them -A **place** is a full-screen room in codeaf that is not this conversation. There are seven, -and they are always in the same order: the words on the top line, then the two reached by -their command, with `chats`, the way back to your conversations, third on the line: +A **place** is a full-screen view in codeaf. The main bar groups your daily work first, then utilities: -`home` · `teams` · `chats` · `sessions` · `spend` · `settings` · `standing` · `memory` +`Home` · `Chats` · `AI teams` · `Activity` · `Memory` · `Spend` · `Settings` -**The top line draws six places:** `home teams chats sessions spend settings`. **Teams** is -right after home: your teams, what waits on you from them, and the selected team's manager -conversation (the **Teams page** of this manual has all of it). `chats` is not a room: it is -the way back to your conversations (*The `chats` word on the bar* below). Standing and memory are -places all the same — `/standing`, `/memory`, their digit, the map and the typed -box all reach them — and while you are standing in one its word is on the bar after the -six, so the bar always says where you are. +AI teams groups AI chats around ongoing work, with an optional AI manager. Activity shows work across chats and delegated tasks. Memory is always available, including before anything has been saved. Standing orders remain accessible through `/standing`, their shortcut, and the map; their label appears when that view is open. They are drawn on the **top line of every page**, right after the `codeaf` wordmark, on a place and in a conversation alike. This manual still calls that row of words **the bar**: ``` - >● codeaf home teams chats sessions spend settings 3 moving · $1.20 thu 10:31pm + >● codeaf Home Chats AI teams Activity Memory Spend Settings 3 moving · $1.20 thu 10:31pm ────────────────────────────────────────────────────────────────────────────────────────── ``` @@ -45,7 +37,7 @@ stacked: opening one closes whichever was up, so `esc` is always one press from ## The `chats` word on the bar: how do I get back to my conversation from a place -**`chats` is the third word on the bar, and it is the way back.** Click it, press `alt+3` +**`chats` is the second word on the bar, and it is the way back.** Click it, press `alt+3` (`opt+3` on a Mac), or walk the bar's cursor onto it and press `enter`: the place closes and the conversation that was in front before you opened it is in front again, with its draft and its scroll where you left them. With no conversation open at all, it opens the new-chat @@ -63,11 +55,9 @@ no count. Its hint says `every conversation, one at a time`, which is how it dif Four ways, and they all reach the same seven rooms: - **`tab`** — the next place **on the bar**, round again from the last. **`shift+tab`** — - the one before. From standing or memory, `tab` goes on round the bar to home. + the one before. The order follows the visible bar; Chats returns to the conversation. - **`alt+1`** … **`alt+8`** (**`opt+1`** … **`opt+8`** on a Mac) jump straight to one, **from a - place or from a conversation**. `alt+1` … `alt+6` are the tab bar's own order (home, - teams, chats, sessions, spend, settings), and `alt+7` and `alt+8` are the two places off the bar: - standing and memory. Hold `alt` and press the digit. macOS draws the modifier as `opt` + place or from a conversation**. Shortcuts preserve their established destinations even though the visual order changed: home (1), AI teams (2), chats (3), activity (4), spend (5), settings (6), standing (7), memory (8). Hold `alt` and press the digit. macOS draws the modifier as `opt` because that is the key's name on a Mac keycap; Linux and Windows draw it `alt+`, and it is the same chord either way. `tab` and the shift-arrows are not like them: in a conversation those already belong to path completion and to the caret, so the digits are the one class of @@ -106,7 +96,7 @@ empty room actually wants — see **Every place opens, always** further down thi That is true of every door onto a place and not only of the walk: a number, a click on the word, and the command that names it all open the same room on the same machine. `/history` -and a bare `/task` on a machine that has run nothing open the sessions place, headed `sessions` +and a bare `/task` on a machine that has run nothing open the activity page, headed `activity` over one line: `work you send off with /task lands here, and its record stays`. ## How do I move between the tabs with the arrow keys — the tab bar is a row the cursor can stand on @@ -188,7 +178,7 @@ does nothing; a press on the word you are already standing on does nothing. ## Where did the places go: the top line holds the places, and why chat tabs are not showing on the home page **The places sit on the top line, after the `codeaf` wordmark. The tab strip of your chats -is drawn only while a conversation is in front.** It is not on home, teams, sessions, spend, +is drawn only while a conversation is in front.** It is not on home, AI teams, activity, memory, spend, settings, or any other place. Chat tabs on the home page are not a thing this screen does. `chats` on the top line, `alt+k`, and home's sessions list are how you get to one. @@ -196,7 +186,7 @@ The places used to be a row of their own under the top line on a place, and the took that same row inside a conversation. For a while after that the strip stayed on every page, which repeated the teams rail and home's sessions. Now: -- **The top line** is the wordmark, the six places, and the machine's signs on the far end. +- **The top line** is the wordmark, the seven main destinations, and the machine's signs on the far end. It is identical on every page; only which word is lit changes. It never moves. - **The second line of a chat** is the tab strip: the team chip (` ● harbor ▾ `, or a quiet ` Teams ▾ `), `◆ Manager`, your tabs, `+` and `▦ All`. It has no `home` piece, since home @@ -255,7 +245,7 @@ six of the seven places that box was a lie in a slot: `enter` opened rows, typin went to the filter at the top of the list, and on spend the words went nowhere at all. It is gone (2026-09-17). -**Typing still filters where a list is worth filtering.** On **sessions** every printable key +**Typing still filters where a list is worth filtering.** On **activity** every printable key narrows the conversation list by conversation name, project name or path, or nested task name. Matching conversations keep their complete task trees. The letters draw on the control row at the top beside the `⌕` mark. On **memory** the head row echoes the filter in place of @@ -422,7 +412,7 @@ The last two classes are bound where there is something to bind. `alt+` `alt+s` on the memory place, which changes which shelf it is showing; home's panels have no second shape, so `alt+g` and `alt+q` do nothing there. `shift+` is a place's time window — `shift+←→` moves it by its own length, `shift+↑↓` changes how coarse it is — and -three places have one: **sessions** (when it ran), **standing** (when it fired) and **spend** +three places have one: **activity** (when it ran), **standing** (when it fired) and **spend** (which days). All three draw the same control on their own head row, at the right of the line: `shift+← aug 12 – aug 25 →`, with `shift+↑ coarser` beside it where the line has room. The label between the arrows is the control and the reading at once, so the span is on the @@ -518,8 +508,7 @@ Neither of them ever ends in `…`, and neither ever cuts inside a word. Press `alt+.` and the whole key map appears **in the cells you were already reading**: -- the tab bar's words grow their numbers: `1 home`, `2 teams`, `3 chats`, `4 sessions`, - `5 spend`, `6 settings`. The two places off the bar follow: `7 standing`, `8 memory`. +- the bar shows the established shortcut numbers beside each destination: `1 home`, `3 chats`, `2 AI teams`, `4 activity`, `8 memory`, `5 spend`, `6 settings`, followed by `7 standing`. Visual order does not change shortcut destinations. - the hint line becomes the chord list `?` over an empty box draws the same map, which is what that key means on a place — show me @@ -558,7 +547,7 @@ bullets on the conversation or task, with no separate `needs you` heading. ## teams: your teams, what waits on you, and each team's manager -The second place on the bar, right after home. On the left is a **rail**: your teams as a +The AI teams destination follows Chats on the bar. On the left is a **rail**: your teams as a tree (a sub-team indented under its parent), each with its colour, and a mark only when something is happening in it: a dim `⠿` while one of its members is working, and an amber `? 2` while two things wait on you. The sidebar keeps `+ New team` after the teams list and a `Show closed · N` toggle at its bottom. @@ -568,7 +557,7 @@ cards. Decisions and permissions remain actionable, and exchanges expand inline. team selected. Selecting the team itself leaves the current chat and draft untouched. `/teams`, `alt+2` and a click on `teams` open it. The **Teams page** has the details. -## sessions — the sessions page for tasks and history, and how to get to it without a command +## sessions — the activity page for tasks and history, and how to get to it without a command The full-screen conversation tree groups every chat and its tasks into **running** and **completed**. A conversation stays under running while it is answering or has running, @@ -756,12 +745,7 @@ the line): 6. and then the day's figure itself. **The count of things waiting on you never goes.** `2 ?` outlasts the money and the places, -at every width. At 80 columns all six places fit beside `2 ? · $1.20 / $20`. At 60, with -nothing waiting on you, they read - -``` - >● codeaf home teams chats sessions more ▾ $1.20 -``` +at every width. With activity and spending to show, an 80-column bar keeps daily work and Memory visible, while Spend and Settings remain under `more`. The menu preserves their names and shortcuts. Wider windows show all main destinations; narrower windows progressively fold them while keeping the current destination visible. **`more ▾` is a door.** Click it and a small menu hangs under it listing exactly the places it folded, each with its key (`spend alt+5`, `settings alt+6`). The pointer lights a row and a @@ -789,7 +773,7 @@ do. It never says the place is empty. or any of the eight assigned digits, is a key that does nothing. On a machine codeaf was installed on an hour ago, `alt+4`, `alt+7` and `alt+8` all open: -- **sessions**, headed `sessions`: +- **activity**, headed `activity`: `work you send off with /task lands here, and its record stays` - **standing**, headed `standing orders`: `reminders, watches and routines · "remind me at 6" or "every morning at 9"` @@ -863,7 +847,7 @@ own remote stores when the engine supports them: |---|---| | **home** | the far machine's projects and conversations | | **teams** | the far machine's teams, their packets and their spend; the team card's settings rows are that machine's, and `Wrap up first` is offered only when its engine answers the wrap-up doors | -| **sessions** | the far machine's work, out of the same reading | +| **activity** | the far machine's work, out of the same reading | | **standing** | the far machine's orders — both what stands on this conversation and what stands anywhere else on that machine | | **settings** | this computer's rows; the sheet says the far conversation reads its profile on the other machine | | **spend** | the far machine's priced model calls | @@ -906,7 +890,7 @@ something true to say. **Yes — the far machine's work, and none of this one's.** -The sessions place reads its rows out of the same reading home lists, so the door that carried +The activity page reads its rows out of the same reading home lists, so the door that carried home carried this too. `/history`, `ctrl+.` and the tabs all open the same page. This was the worst of the seven before it crossed. The page walked *this* computer's diff --git a/internal/manual/chat/questions.md b/internal/manual/chat/questions.md index ba11f42c38..7fcf5dd143 100644 --- a/internal/manual/chat/questions.md +++ b/internal/manual/chat/questions.md @@ -297,7 +297,7 @@ you, or `/autonomy decide`. **How do I turn that off:** `/autonomy puts it back to asking you every time, and `/autonomy` on its own shows every row as it stands. -**Open settings** (`ctrl+,` or `/settings`) and look under **Safety** for +**Open settings** (`ctrl+,` or `/settings`) and look under **Permissions** for `questions while you are away`. There is a row per kind of question, and `enter` walks the answer round three words: @@ -719,7 +719,7 @@ With more than one waiting it counts them instead — `? 3 questions · alt+y`. ratify line is never counted: nothing is waiting on it. The chip never takes the keys off the page you are standing on. A task that -raised a question while you were on the sessions place says so at the right of the +raised a question while you were on the activity page says so at the right of the foot, and the row's own `enter open its room` stays where it was. **`alt+y` brings the newest one back** and takes you to the conversation it @@ -835,7 +835,7 @@ minute has passed — that line is news, and coming back later is later. The decision itself is in the transcript and in `decisions.jsonl` for good. **A question you have NOT answered is still there when you come back**, wherever -you went — another tab, home, the sessions place. That is the point of leaving it: +you went — another tab, home, the activity page. That is the point of leaving it: nothing about the work moves while it waits, and the chip in the status line counts it from every page. @@ -1117,7 +1117,7 @@ have one is to close this and talk. ## Can I just let it decide — stop asking me this kind of thing **There is a row for it where settings are browsed.** Open settings (`ctrl+,`, -or `/settings`), go to **Safety**, and the section *questions while you are +or `/settings`), go to **Permissions**, and the section *questions while you are away* has one row per kind of question — permission, choice, judgement, landing, assumptions — each saying `ask me`, `recommend then go · 30s` or `decide yourself`, and `enter` walks it round the three. Searching settings for "away" diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index f1faa2be10..0d61e2c98e 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -3,14 +3,14 @@ ## Is there a newer version — update a dev build — keep a dev build up to date — latest dev — /update — why does it say this every time I start At launch, a stable, dev or staging build behind the newest release of its own -channel is told about it, and with `auto update` on (the default) the install +channel is told about it, and with `automatic updates` on (the default) the install begins on its own after a brief pause: the sentence `codeaf is out · installs in 10s · skip · /update` waits in the keys line, counted down from about ten seconds. It never blocks a keystroke. `/update` while it is up installs immediately — that is the release the offer is about, not a rollback; the skip chord, or `/update skip`, answers that one release, it is never offered again, and a later release is a new question; `/update never` turns -`auto update` off, and so does the `auto update` row on `/settings`' **Workspace** tab. Turning it off does not +`automatic updates` off, and so does the `automatic updates` row on `/settings`' **General** tab. Turning it off does not cancel an install already downloading, and the launch check still makes its one request and names what is out. A release whose automatic install failed three times stops being tried on its own. @@ -20,10 +20,10 @@ or ahead build gets nothing. Source and unstamped builds make no launch request, and their `/update` answers `this codeaf was built from source · rebuild with make build, or install a release: curl -fsSL https://agentfield.ai/get/codeaf | bash`. A codeaf owned by Homebrew, Nix or the system package manager, or one in a -folder this account cannot write, is never replaced in place: with `auto update` +folder this account cannot write, is never replaced in place: with `automatic updates` on or off the launch names the manager and the curl line, not `/update`. -With `auto update` off, the launch check still runs and still says `codeaf +With `automatic updates` off, the launch check still runs and still says `codeaf is out · you have · /update installs it for the next launch`, but nothing is downloaded until you ask. diff --git a/internal/manual/chat/running-on-another-machine.md b/internal/manual/chat/running-on-another-machine.md index 59ddf253c8..fb4b7a410a 100644 --- a/internal/manual/chat/running-on-another-machine.md +++ b/internal/manual/chat/running-on-another-machine.md @@ -460,13 +460,13 @@ exact sentence each one says. travels on the wire like every other answer; nothing about it needs a browser or a port. This is the one entry on the list that is a capability, not a limit. -5. **`/settings` opens anyway, and says whose rows these are.** Every tab but Teams writes - this machine. The Teams tab is saved on the other machine when that machine can take the +5. **`/settings` opens anyway, and says whose rows these are.** Every category but AI teams writes + this machine. The AI teams category is saved on the other machine when that machine can take the change. Opening on any other tab says exactly: - `these rows belong to this machine; the Teams tab is saved on the other one.` - On the Teams tab it says only what is true there: + `these rows belong to this machine; the AI teams category is saved on the other one.` + On the AI teams category it says only what is true there: `these rows are saved on .` - An older engine, where the Teams tab cannot be saved over the connection, says exactly: + An older engine, where the AI teams category cannot be saved over the connection, says exactly: `these rows belong to this machine; this conversation reads its profile on the other one.` ## More of what does not work over --host @@ -687,10 +687,10 @@ browser and no port, so that road stays open over `--host`. `/settings` opens over a connection. On any tab but Teams it says exactly: ``` -these rows belong to this machine; the Teams tab is saved on the other one. +these rows belong to this machine; the AI teams category is saved on the other one. ``` -On the Teams tab it says only what is true there: +On the AI teams category it says only what is true there: ``` these rows are saved on . @@ -703,13 +703,13 @@ these rows belong to this machine; this conversation reads its profile on the ot ``` The rows on every tab but Teams are this machine's, and those changes apply to what you -are looking at. The Teams tab is the other machine's when the connection can save it. +are looking at. The AI teams category is the other machine's when the connection can save it. **Asking codeaf to change a setting goes the other way.** `settings` and `change_setting` run inside the session, which is on the far machine, so they read and write that machine's profile, which is the profile the conversation actually obeys. So over a connection the two doors land in two different files: the panel edits this laptop, and -asking edits the machine the work is on. The Teams tab is the exception on the panel. +asking edits the machine the work is on. The AI teams category is the exception on the panel. It saves on the other machine, the same place asking would write a team default. ## Can I change the Teams settings on another machine, and where do team defaults go over a connection diff --git a/internal/manual/chat/screen.md b/internal/manual/chat/screen.md index 83231e3e49..311a05d040 100644 --- a/internal/manual/chat/screen.md +++ b/internal/manual/chat/screen.md @@ -105,7 +105,7 @@ navigation from reading. The strip is that chat's row. Home, teams, sessions, sp settings and every other place do not draw it: ``` - >● codeaf home teams chats sessions spend settings $1.20 / $20 · thu 10:31pm + >● codeaf home chats AI teams activity memory spend settings $1.20 / $20 · thu 10:31pm openrouter price scrape Refactor the rail scope... Shipping the parser × + ▦ All ────────────────────────────────────────────────────────────────────────────────────────────────── ``` @@ -1187,7 +1187,7 @@ expire on time. ## Why the bottom rows of a long list look dimmer — faded, greyed out or washed out rows The last three rows of a list that runs on past the bottom of its window are drawn a step -fainter each, fading toward the background. It happens on the sessions place (`/history`, +fainter each, fading toward the background. It happens on the activity page (`/history`, `ctrl+.`) and on the task column. Home's panels never fade: each one folds inside itself with `N more` instead. @@ -1436,7 +1436,7 @@ The top line is the program's name and the places on the left, and the machine's signs on the right: ``` - >● codeaf home teams chats sessions spend settings 2 want you · 4 moving · $0.55 / $20 · thu 1:11pm + >● codeaf home chats AI teams activity memory spend settings 2 want you · 4 moving · $0.55 / $20 · thu 1:11pm ``` When there is not room for all of it, things give way **one at a time, in a fixed order** @@ -2389,7 +2389,7 @@ than the sentence it belongs to. ## Proper icons, missing icons, empty boxes, Nerd Font and the step icons setting The normal view uses the Font Awesome icons included in Nerd Fonts. Under -`/settings` → Display → **step icons** (`ui.icons`), `auto` chooses those icons +`/settings` → General → **tool icons** (`ui.icons`), `auto` chooses those icons unless terminal detection calls for plain symbols. Known console and locale limitations fall back; colour depth alone does not remove icons. @@ -3240,7 +3240,7 @@ Finished work sits under `Done`, which starts folded to its heading: press `Done `enter` on it, to open it, and it stays open for the session. A background job is not a finished task row: it lives in the `jobs` section, and its log -path is on the job's page. The full record of any task is on the sessions place (`ctrl+.`, +path is on the job's page. The full record of any task is on the activity page (`ctrl+.`, `/history`). ## Scrolling the task column: the mouse wheel over the sidebar, and the keys that walk it diff --git a/internal/manual/chat/senior-dev.md b/internal/manual/chat/senior-dev.md index 258a386a00..faa085e6e5 100644 --- a/internal/manual/chat/senior-dev.md +++ b/internal/manual/chat/senior-dev.md @@ -178,7 +178,7 @@ A task handed to senior-dev wears its name as a badge wherever a task is named: same number in this conversation wears. - **The task strip**, the row of chips that stands in for the side list under 100 columns, wears `[sd]`. -- **The `@` list, the sessions place and home** — its list of work, a landing under +- **The `@` list, the activity page and home** — its list of work, a landing under `needs you` and a line under `since you left` — wear `[senior-dev]`, or `[sd]` where the row is short of room. The title is cut before the badge, and a `since you left` line cuts what the work came to first: `rewrite the auth middleware [senior-dev] · it…`. @@ -1116,7 +1116,7 @@ tool, the conversation list's task counts and every other codeaf window on the p see it, and a window that has the run's conversation open says it is being worked on. The conversation that started the run lists it once, by the number its rail shows. -**Another window can watch it, read-only.** On that window's sessions place the run's row +**Another window can watch it, read-only.** On that window's activity page the run's row stands under `running` with `another window` beside it, and `enter read it as it runs` opens the page the conversation that started it shows: senior-dev's actions under their steps, the line over them with the step, the spend, the calls and the time, and `ctrl+y` diff --git a/internal/manual/chat/services.md b/internal/manual/chat/services.md index e30d4c9ac6..959c559eb1 100644 --- a/internal/manual/chat/services.md +++ b/internal/manual/chat/services.md @@ -16,7 +16,7 @@ Pick a row and answer its fields. A successful listed provider says `deepseek-direct is connected · 6 models`; one without a list says only `deepseek-direct is connected`. A provider with more than one billing door names the one it bound: `z-ai-direct is connected · coding plan · 4 models` or -`z-ai-direct is connected · pay-as-you-go · 10 models`. The Providers tab in `/settings` +`z-ai-direct is connected · pay-as-you-go · 10 models`. The Connections category in `/settings` then shows the provider, door, safe spelling of its key, region and order. The default provider remains first. With two or more providers, `/model` groups models by @@ -39,7 +39,7 @@ default provider does have a key, those calls keep using their configured models ## What model do I get after connecting a provider — why did my model change -A successful connection from `/connect`, or a reconnect from the Providers tab in +A successful connection from `/connect`, or a reconnect from the Connections category in `/settings`, moves this conversation onto that provider in the same moment. A plan door's first documented model wins. Otherwise codeaf uses the vendor's preferred model when the provider listed it or published no list, then the first model the provider listed. With no @@ -204,7 +204,7 @@ The default provider's model ids remain unchanged and unqualified. A model from provider is written `/`, such as `deepseek-direct/deepseek-v4-pro`. That first segment is how the conversation remembers where the model can be reached. With two or more connected providers, `/model` shows a dim -heading for each provider, default first, in the order shown in the Providers tab. A +heading for each provider, default first, in the order shown in the Connections category. A custom provider's heading is the name you gave it. The status line uses the same spelling: an unqualified default-provider id, and @@ -259,7 +259,7 @@ available spelling (`localhost-direct`, then numbered ones) and the connect line what it used. That name is the provider everywhere. It is the row's name in `/connect` and on the -Providers tab, the heading its models sit under in `/model`, and the first segment of +Connections category, the heading its models sit under in `/model`, and the first segment of every model id it serves, so a model on a provider named `homelab` reads `homelab/glm-5.3` and `/model homelab/glm-5.3` moves onto it. A refusal or a success names the provider by the name it was given; neither switches back to `custom`. @@ -270,7 +270,7 @@ becomes that first provider's edit door once one is connected and a `+ add a provider` row connects a new one; with none connected yet, **Custom OpenAI-compatible API** is the door onto the first. -On the Providers tab in `/settings` each custom provider is a row of its own. `enter` +On the Connections category in `/settings` each custom provider is a row of its own. `enter` opens its available actions. Custom providers offer refresh, rename, change key, and disconnect; rename reopens the address and name with their saved values. An empty key box keeps the saved key. A changed name is a rename: every model id already picked under the old diff --git a/internal/manual/chat/sessions-and-rewind.md b/internal/manual/chat/sessions-and-rewind.md index 0b9147a11f..3dffdda771 100644 --- a/internal/manual/chat/sessions-and-rewind.md +++ b/internal/manual/chat/sessions-and-rewind.md @@ -899,8 +899,8 @@ are `enter choose · esc cancel`. Cancel changes nothing. A conversation that manages any active ordinary team cannot be deleted. After `delete`, the confirmation names every active team it manages: `This conversation manages . Assign another manager before deleting it.` Go to Teams, select each -named team, and use `Choose manager` to select an existing member. To use a new -conversation, first use `+ Add member`, then `Choose manager`. Return to Home, Chats or +named team, and use `Choose AI manager` to select an existing member. To use a new +conversation, first use `+ Add chat`, then `Choose AI manager`. Return to Home, Chats or Sessions to delete the former manager. Deletion never appoints a manager, creates a conversation or disbands a team. Tasks inside manager conversations can still be deleted independently. The optional global-manager conversation can be deleted without a replacement; diff --git a/internal/manual/chat/settings.md b/internal/manual/chat/settings.md new file mode 100644 index 0000000000..9a69f926d5 --- /dev/null +++ b/internal/manual/chat/settings.md @@ -0,0 +1,143 @@ +# Settings + +## Find a setting — categories, search and advanced controls + +Open `/settings` or press `ctrl+,`. Settings is organized into General, Models, +Memory, Tasks, AI teams, Permissions, Spending, Connections and Privacy. Wide +terminals keep the categories beside the controls; narrow terminals show a +category bar. Left and right change category, up and down choose a row, and +Enter changes it. The pointer can select the same controls. + +Type to search across categories, including advanced controls and connection +services. Old setting names and configuration keys remain searchable. Clear the +query to return to browsing; Escape clears it first, then closes settings. Model +pickers have their own **Filter models** box. Each category keeps uncommon controls behind +`Advanced`; search can find them without opening that section first. + +Selected-row help explains the setting. Scope and activation details appear +where known. A value controlled by an environment variable remains read-only; +change the variable at launch rather than trying to override it in Settings. +On/off controls change when activated. Settings with several values open a choice +list, with the saved value marked; opening or cancelling the list writes nothing. +Choose an option to save it. Text and number editors keep the setting explanation, +input and Save/Cancel controls together. Enter saves; Escape or Cancel discards +the draft. Invalid input stays beside its error without changing the saved value. +Any restart requirement remains visible while editing. A successful write shows +Saved. Remote AI team defaults wait for the host's acknowledgement; after Save +has been sent, the editor says Saving and offers Close. Closing that editor does +not undo the write already in progress. + +## Change settings by asking in chat + +You can ask codeaf to change supported preferences: “hide hints,” “expand completed +tool details,” “turn timestamps off,” or “remember across chats.” It reads the +`settings` tool first, then uses `change_setting` to save the requested value +through the same registry as the panel. If these tools are not loaded, codeaf +loads the `settings` capability group. It does not need to edit config files. + +Chat discovery uses the same categories and names as the panel, including old +names as aliases. Tool details pair readable choices with the exact values to +write; those raw values can differ from their labels. In particular, the legacy +`ui.hints` row means **disable hints** to the registry, so its raw `on` hides tips +and raw `off` shows them. codeaf should follow the returned mapping, not guess +from the positive “show hints” label. + +Saved preferences appear when Settings is opened again. Interface preferences +changed by chat refresh when the turn finishes; changes made directly in Settings +refresh the interface immediately. Startup-only settings still need the CLI +restart stated in their receipt. Chat-written routing, speed guard and pinned-host +preferences also recommend restarting the CLI to ensure already-open chats use +them; the panel has separate live controls for those settings. Saving memory +preferences does not reopen a +memory store already held by a running chat. + +Some controls remain yours to change in Settings: approvals, spending limits, +task concurrency, review safeguards and credentials. Task workers do not have +the settings tools. A refusal should be explained rather than bypassed with a +file edit. Over `--host`, the chat changes its host's profile; the local Settings +panel and remote AI team defaults retain the machine boundaries described in +the commands manual. + +## Did the update reset my settings — old profiles and existing preferences + +No configuration migration is needed for the new categories. Existing profile +keys, saved values, project overrides and environment precedence keep their +meaning. The same reader, validator and writer still apply each registry setting. +For example, `show hints` presents the positive choice while preserving the +existing stored preference. Renaming or moving a control does not reset it. + +Some controls previously visible in chat settings belonged to the separate +resident: practice idle time, practice spending, arrival briefs and tenure. +They are omitted here instead of suggesting that changing them teaches this chat. +Their stored values remain intact. Internal model slots that this surface cannot +change are omitted; supported model controls remain in Models. + +## General — interface preferences and hints + +**General** contains mouse interaction, completed tool details, tool icons, chat +switching, the task sidebar, updates and background reminders. **Show hints** is +positive: on shows contextual tips, off hides them. Keyboard instructions and +setting explanations stay visible either way. **Completed tool details** offers +**collapsed** or **expanded** without changing the meaning of an existing profile. + +## AI teams settings versus an individual AI team + +**AI teams** contains defaults for groups of AI chats, including questions to an +AI manager, turns started by team messages, spending and subteam behavior. It +does not configure human members or shared user accounts. A team may have no +manager, and a chat can belong to several teams. + +An individual team's own Settings controls its supported overrides. Global +defaults do not erase explicit team overrides. New-subteam budget share applies +when a subteam is created; it does not redistribute existing budgets. + +## Memory settings — saved versus active, inspect and forget + +**Memory → remember across chats** controls the saved memory preference. It +supports learning useful preferences and corrections from conversations without +requiring every memory to be dictated. Use `/memory` or `/memories` to inspect +saved memories; the memory page offers its supported correction and forgetting +controls. A setting being on is not proof that any particular fact was saved. + +Already-open chats keep the memory store they opened with. Restart the CLI to +apply a changed preference to those chats. Turning memory off does not delete +saved memories. Input history and unfinished drafts are separate Privacy choices. + +## Permissions, task timers and automatic work + +**Permissions** controls tool approvals and exceptions. Dangerous-command +exceptions still apply in permissive modes. An approval timer pauses when it +expires and keeps waiting; it does not silently approve a tool. + +The proposed-task start timer has a different consequence: it starts the proposed +work when it expires. Read each control's consequence before changing its duration. +**Tasks** contains scope assessment, review, repair and concurrency; machine +resource limits are advanced controls. A one-off task does not require an AI team. +Under Advanced, **task scope assessment** offers **assess while working** or **skip assessment**. +Both start work immediately; assessment helps identify work to delegate. + +**Concurrent tasks: no limit** means no user-set count cap, not disabled tasks. +A positive whole number caps simultaneous tasks, with the rest queued. Blank +restores no limit. CPU, available memory and provider limits can still hold new +work. Invalid input stays in the editor and does not change the saved preference. +The current engine reads the count cap at startup: restart the CLI to apply a +changed cap to already-open chats. Changing this preference does not stop a +running task. An unset task model follows the configured worker, falling back to +the current chat model when no worker model is set. + +## Connect a service, change models or manage privacy + +**Connections** contains service sign-in and provider credentials, web search +configuration and advanced remote-connection or custom OAuth application settings. +Ordinary account connection does not require you to create your own OAuth app. +**Models** selects supported models and reasoning effort; advanced controls hold +routing, internal role overrides and context limits. + +**Privacy** separates input history, unfinished drafts, anonymous usage reporting +and **shared model recommendations** (formerly Model Pool). Recommendations can +**use and contribute**, **use only**, or remain **off**. Disabling one privacy +control is not a promise that all the others are disabled. Search accepts old +labels as well as the new organization. + +Resident-only practice controls remain in resident configuration. They do not +appear in chat settings, and `/budget practice` cannot edit them from chat. diff --git a/internal/manual/chat/staying-on-that-machine.md b/internal/manual/chat/staying-on-that-machine.md index c7707d7051..ecdddc7f6b 100644 --- a/internal/manual/chat/staying-on-that-machine.md +++ b/internal/manual/chat/staying-on-that-machine.md @@ -60,11 +60,7 @@ lives under this machine's codeaf state directory at `~/.codeaf/v3/ssh/` (moved `CODEAF_HOME`). The same **103-byte** socket-path limit applies there: a state path too long disables reuse only; the ordinary ssh connection still opens. -These network-dependent defaults are editable on `/settings`' **Workspace** tab as `ssh -reuse` (`300s`), `ssh heartbeat` (`3s`), `ssh missed heartbeats` (`3`), and `ssh traffic` -(`lowdelay`). They were on the Session tab and moved, because every one of them lands on -the next launch rather than on the conversation in front of you; nothing you saved moved -with them. Setting the heartbeat to 0 turns +These network-dependent defaults are under `/settings` → **Connections** → **Advanced**: `SSH connection reuse` (`300s`), `SSH keepalive interval` (`3s`), `SSH missed keepalives` (`3`), and `SSH traffic priority` (`lowdelay`). Existing saved values are preserved. They apply to new connections. Setting the heartbeat to 0 turns dead-link probes off; setting reuse to 0 stops keeping a connection after its channel closes. Whole-stream ssh compression stays off because it usually slows a LAN attach; large transcript frames compress themselves only when both codeaf builds support it. diff --git a/internal/manual/chat/task-controls.md b/internal/manual/chat/task-controls.md index 5dee0eb6a3..94a98137c8 100644 --- a/internal/manual/chat/task-controls.md +++ b/internal/manual/chat/task-controls.md @@ -157,7 +157,7 @@ does not repeat the request or change the transcript's presentation. ## Reading a note on a run task's room, and the room following the newest step A run's task opens the same task room as any other task: from its row on the side list, -from `enter` on its row in the sessions place, or from the run's tab. The room keeps up with +from `enter` on its row in the activity page, or from the run's tab. The room keeps up with its newest step, reading the task again every three seconds while it is queued or running, and follows it until you scroll up; scrolling back to the bottom resumes the follow, and a room on a settled task is a still page. Its box says `a note for this task`: type in it and @@ -174,7 +174,7 @@ step, so its room takes no note: `enter` there says `this task has finished` and words can go, and leaves them in the box. `x` is read only over an empty box: the moment there is a note to type, a letter is a letter. `x` raises the `Stop this task?` card before anything ends. On a task that has ended it is not offered and does nothing: it is a letter in -the box. `p` is always a letter in the room; a part is held from its row in the sessions place. +the box. `p` is always a letter in the room; a part is held from its row in the activity page. ## Typing while a task's room is still loading: the letters land in its box diff --git a/internal/manual/chat/task-rooms-after-restart.md b/internal/manual/chat/task-rooms-after-restart.md index ce7d2e75db..e861106abc 100644 --- a/internal/manual/chat/task-rooms-after-restart.md +++ b/internal/manual/chat/task-rooms-after-restart.md @@ -95,7 +95,7 @@ this task has finished — say it to main The header reads the same record, keeping the name, state and elapsed correct. -The room is one of two doors onto old work. The other is the sessions place (`ctrl+.`, +The room is one of two doors onto old work. The other is the activity page (`ctrl+.`, `/history`), whose `enter` on a completed task's row opens a card with the task's report read off the same journal — and which says `its transcript is not on this disk any more` in the same case. If the room is empty, the card will be too; the file is the same file. diff --git a/internal/manual/chat/tasks.md b/internal/manual/chat/tasks.md index a4d5dd7355..53f74101b9 100644 --- a/internal/manual/chat/tasks.md +++ b/internal/manual/chat/tasks.md @@ -521,8 +521,8 @@ because its row is read as a design and not as a task. ## Stopping the sizing call — the starting a task setting, making one worker the default `/task ` asks you nothing, and every answer here starts **one worker**. What the row -decides is only what is paid to find out how wide the work is. `/settings` → Session → -**starting a task**, or the `task.start` row: +decides is only what is paid to find out how wide the work is. `/settings` → Tasks → +**task planning**, or the `task.start` row: - **sized** — the default. One worker starts at once and the sizing call reads your brief **beside** it; where it finds independent parts in your words, they are weighed and @@ -1050,7 +1050,7 @@ handed to the model that has just written the answer and would be editing itself is set for them, they have no model at all, and this is the sentence that says so. **What to do about it.** Pin a planner with `/crew pin planner `, or pin the two roles on the -**pinned roles** row in `/settings` → Providers — they are called `markreader` and `handoff`, +**pinned roles** row in `/settings` → Models — they are called `markreader` and `handoff`, so the row reads `markreader:openai/gpt-5, handoff:openai/gpt-5`. Or run with `--one-model`, which settles them on the model you are talking to along with everything else. Either way the move still happens — a task that starts knowing only what you typed is better than an answer @@ -2059,7 +2059,7 @@ tree of who started whom; the task's own page (its kin line) and the task page h family. **A run's rows are on this column too.** Under the conversation that started a run, the -column draws that run's tree out of the sessions place's reading: one line per task (the +column draws that run's tree out of the activity page's reading: one line per task (the connector, the state mark and the fitted title) with `waits: ` at the end of a line held behind named work, and the run's own row ending in the dot row (*what are the dots next to a task?* has the cells). While a task's worker is on a step, its row spends one @@ -2384,7 +2384,7 @@ work, shapes the brief and starts it. The column's other section, `standing`, en ## What a bare /task does — /task with nothing after it opens the task page -**`/task` typed on its own opens the full-screen sessions place** — the same place `/history` and +**`/task` typed on its own opens the full-screen activity page** — the same place `/history` and `ctrl+.` open, holding this machine's conversations and their tasks. It used to print a one-line usage instead. It does not any more. @@ -2392,7 +2392,7 @@ The reason is the `+ /task` row at the foot of the task column: that row puts `/ your box before you have said what the work is, so a `/task` sent as it stands is asking the only question the word can answer with no brief behind it — *what work is there.* -**On a project that has never run a task it opens the page anyway**, headed `sessions` over one +**On a project that has never run a task it opens the page anyway**, headed `activity` over one line — `work you send off with /task lands here, and its record stays` — which is exactly what `/history` and `ctrl+.` do there too. @@ -2400,7 +2400,7 @@ what `/history` and `ctrl+.` do there too. still size, shape and start the work directly, with no proposal card in between and no extra question. There is no third form: `/task adaptive` is retired. -**There is still no `/tasks` command**, though `sessions` is the name of the PLACE `/history` +**There is still no `/tasks` command**, though `activity` is the name of the PLACE `/history` opens, `alt+4` and `tab` get there without typing anything. As a slash word the plural is not one this surface answers to; the two things a bare `/task` and a `/task ` do are the pair of errands a person has about tasks — go and look at the work, or give codeaf some. @@ -2416,7 +2416,7 @@ ctrl+. earlier ``` - **It is a door and not a note.** Press `ctrl+.`, or click that line, and the full-screen - sessions place opens with every task this machine has run on it, grouped by what you do + activity page opens with every task this machine has run on it, grouped by what you do next: `running` and `completed`. `/history` is the same page. - **It is drawn only when there is something behind it**, and never as `0 earlier` or any other count of nothing. There is only ever one such line. @@ -2433,11 +2433,11 @@ conversation's work into another one's without the column ever saying it had. Ev they offered is on the other side of the door, whole: every row, the filter, the cards, and `m` for the mention. -**Where old work is listed now:** the sessions place (`ctrl+.`, `/history`, or that line), and +**Where old work is listed now:** the activity page (`ctrl+.`, `/history`, or that line), and home (`/home`, or space twice on an empty box). The chat can also read the whole project record for you with its `tasks` tool; just ask. -**Running work in another codeaf window** is on no surface but the sessions place. An ordinary +**Running work in another codeaf window** is on no surface but the activity page. An ordinary task writes nothing into the project's file until it lands, so the window next door is the only place that work can be read from, and `/history` is the page that reads it. @@ -2484,7 +2484,7 @@ you need to know a chord to recover. See *The task bar disappeared* below. The choice is remembered. It is written to your profile the moment the column moves, as the `ui.task_column` setting, which also appears in the settings panel (`ctrl+,`) on the -Display tab as **task column**. A change made in the panel lands the next time codeaf +General category as **task sidebar**. A change made in the panel lands the next time codeaf starts; `alt+l` acts immediately and wins for this session. On the teams page the column starts folded, because that page has a rail of its own on the left, and `alt+l` there opens it for that visit only. @@ -2570,7 +2570,7 @@ server still has the jobs section to put a cursor on, so `alt+t` takes it. **The walk stops at this conversation's last job, after its last task.** `↓` walks the band, the groups and then the jobs section under them, and clamps there rather than carrying on -into the project's record. Old work is walked on the sessions place (`ctrl+.`), where `enter` +into the project's record. Old work is walked on the activity page (`ctrl+.`), where `enter` goes inside its card. The cursor follows the task, not the row, when a task moves from one group to another. If @@ -2625,7 +2625,7 @@ nothing else, so a task you ran last week, in a session you have closed, is nowh screen until you open this. **There is no `/tasks` command** — though `sessions` is what the PLACE this opens is called on -the tab bar, reached with `alt+4` or `tab`. `/task ` starts work; `/history` opens the same sessions place +the tab bar, reached with `alt+4` or `tab`. `/task ` starts work; `/history` opens the same activity page started — and so does a **bare `/task`**, which opens this very page rather than printing a usage line. The page is also reached from the one dim door line at the bottom of the task column, `ctrl+. earlier`, whenever the project has work this conversation never ran. @@ -3184,7 +3184,7 @@ originating conversation and available evidence. Press `esc` to return to the li ## Going inside an old task — see what a past task did, read a finished task's report, where is the story my task wrote -`enter` on any task row of the sessions place (`ctrl+.`, `/history`) that this conversation did not +`enter` on any task row of the activity page (`ctrl+.`, `/history`) that this conversation did not run **goes inside that task**. A click does the same on the first press. The task column carries no rows of old work — its `ctrl+. earlier` line is the door onto this page — so the page is where every old task is opened. @@ -3271,7 +3271,7 @@ one more dim line: ctrl+. earlier ``` -Click it, or press `ctrl+.`, and the full-screen sessions place opens. The column is left exactly +Click it, or press `ctrl+.`, and the full-screen activity page opens. The column is left exactly as it was: the page is somewhere you go and come back from, not a state the column enters. **There is exactly one such line, never two.** It is drawn when the record holds tasks @@ -4990,8 +4990,8 @@ it was going to be decided in is gone and nothing is going to finish that though ## Can codeaf decide on its own — can the chat decide on its own, stop asking me about tasks that need a look -Yes. The setting is **`task.settle`**, in `/settings` under Session as -`who settles work that needs a look`, and it takes two words: +Yes. The setting is **`task.settle`**, in `/settings` under Tasks as +`when task results need a decision`, and it takes two words: | Value | What happens when a task lands as `your call` | | --- | --- | @@ -5186,7 +5186,7 @@ stop, one word, wherever you reach it from. Pressing `x` on a run that has already finished does nothing but say so. -## The sessions place — tree lines, project column, folds, and the time-window keys +## The activity page — tree lines, project column, folds, and the time-window keys The **sessions** place lists conversations and their nested work across projects, under **running** or **completed**. The whole conversation moves together. Both sections and @@ -5344,7 +5344,7 @@ conversation is the way today. ## Preview a task without opening it — the record beside the list, seeing what a task did, and answering a task from the list with 1 and 2 -On a terminal **110 columns or wider** the sessions place splits: the list keeps the left, a dim +On a terminal **110 columns or wider** the activity page splits: the list keeps the left, a dim rule divides it, and the right is the record of whatever row the cursor is on. Nothing is opened and nothing is lost — walking down with `↑` and `↓` changes what the pane shows, and the list stays exactly where it was. @@ -5457,7 +5457,7 @@ come back exactly as you left them. **`esc` clears the filter first and closes the place second**, which is why the foot says `esc clear the filter` while one is on. -## The foot of the sessions place — x delete, row verbs and stopping work +## The foot of the activity page — x delete, row verbs and stopping work The foot names the actions available for the selected row. `enter` opens its conversation, task room or saved record. `→` opens its verbs. An expandable row diff --git a/internal/manual/chat/team-manager.md b/internal/manual/chat/team-manager.md index 77d98a2da5..e7c3bd341a 100644 --- a/internal/manual/chat/team-manager.md +++ b/internal/manual/chat/team-manager.md @@ -42,18 +42,18 @@ team-agnostic conversations view instead, and the hint says so. ## Making a manager -The **teams page** (`/teams`, `alt+2`) offers `+ Manager` on a team that has none, and +The **teams page** (`/teams`, `alt+2`) offers `+ AI manager` on a team that has none, and `All teams` offers one manager over every team. Choose a team to talk to its manager beside the tree of teams. Managers cost nothing until you make one. A team's manager is pinned at the left of its tab strip and never scrolls away: -- **`+ Manager`**, a quiet button, while the team has none. A press starts a new conversation +- **`+ AI manager`**, a quiet button, while the team has none. A press starts a new conversation in the team's folder and makes it the manager. Its hint says so. Under 100 columns the button leaves the strip; Teams still offers manager controls. -- **Choose manager** on Teams assigns an existing member. To use a new conversation, - first use Add member, then Choose manager. The Chats dropdown and conversation grid +- **Choose AI manager** on Teams assigns an existing member. To use a new conversation, + first use Add chat, then Choose AI manager. The Chats dropdown and conversation grid offer no manager-assignment controls. The manager's place reads **`◆ Manager`**. Its tile in the team-agnostic grid keeps its ordinary title and position. The tab's hint names its team and title. `alt+m` goes to the @@ -159,7 +159,7 @@ over the conversation, and `esc` or `alt+l` takes it off again. Over `--host` teams and the manager work as they do locally. The teams and their Traffic are kept on the machine the conversations run on, and the window reads and writes them there, so the Traffic is the team's own and a manager set from the laptop is the one the far session -follows. Against a far machine running an older codeaf, `+ Manager` and the menus say managers +follows. Against a far machine running an older codeaf, `+ AI manager` and the menus say managers are not available over `--host`, and the column has no Traffic word. ## When a manager starts a member — team_start and approvals @@ -499,6 +499,6 @@ in the current team; ambiguous or missing history is not guessed. One conversation may manage a team and descendants of that team, while remaining an ordinary member of other teams. It cannot manage unrelated teams. The global manager is -separate from every ordinary team's manager. Choose manager or a team move refuses a +separate from every ordinary team's manager. Choose AI manager or a team move refuses a change that introduces conflicting responsibilities, naming the teams. Existing conflicts are retained until the person explicitly chooses replacements in Teams. diff --git a/internal/manual/chat/teams-page.md b/internal/manual/chat/teams-page.md index cb9f59f793..1677e0143d 100644 --- a/internal/manual/chat/teams-page.md +++ b/internal/manual/chat/teams-page.md @@ -1,17 +1,15 @@ -# The teams page +# AI teams ## What the teams page is, and how to open it The **teams page** is where you run your teams: every team you have, what waits on you from them, and an overview of the team you choose. Member cards show activity and recent updates; -interaction links take you into the conversation where an exchange happened. It is the second -place on the tab bar, right after home: `home teams chats sessions spend settings`. Open it with -`/teams`, `alt+2` (`opt+2` on a Mac), a click on the word `teams`, `tab` from home, or the map +interaction links take you into the conversation where an exchange happened. It follows Chats on the tab bar: `Home Chats AI teams Activity Memory Spend Settings`. Open it with +`/teams`, `alt+2` (`opt+2` on a Mac), a click on `AI teams`, `tab` from Chats, or the map (`alt+.`). A number beside the word on the bar counts the decisions waiting on you that arrived since you last looked. -A team is a group of conversations you name, and a team's **manager** is a conversation that -runs it for you (the **team manager** page). The conversations view (`alt+v`) is where you see +An AI team is a named group of AI chats, not a shared account or a group of human users. A chat can belong to several teams. An optional **AI manager** is a chat that coordinates the group (the **team manager** page). The conversations view (`alt+v`) is where you see every open conversation at once; All teams on this page is where you organize your teams. ## Where a team link in a chat opens @@ -32,7 +30,7 @@ The left column is the **rail**: ● harbor ◆ ? 1 │ ● orbit ⠿ │ ● docs │ - + New team │ + + New AI team │ │ ☐ Show closed · 2 │ ``` @@ -47,8 +45,8 @@ The left column is the **rail**: working; an amber `? 2` says two things wait on you from it or from a team under it: a decision addressed to you, or a member stopped on a question only you can answer. A team where nothing is happening draws no mark at all. -- **`+ New team`** stays visible below the active team list and always creates a top-level team. - **`+ Add subteam`**, beside and after **`+ Add member`** in a selected team's header, +- **`+ New AI team`** stays visible below the active team list and always creates a top-level team. + **`+ Add subteam`**, beside and after **`+ Add chat`** in a selected team's header, creates inside that team. The depth limit still applies. - **`✦ Organize`** in the All teams header suggests groupings and disbanding quiet teams. Its dialog stays on Teams; nothing changes until Apply. - **`Show closed · N`** is a toggle at the bottom of the rail. Turn it on to include retained @@ -152,7 +150,7 @@ for a remote conversation. ## Adding and removing team members -`+ Add member` on the selected team's overview opens a searchable conversation picker. +`+ Add chat` on the selected team's overview opens a searchable conversation picker. It lists existing saved conversations across projects, including ones already in other teams; members of this team are left out. Type a title or workspace to filter, use arrows and `enter`, or click a row. Adding an existing conversation keeps its context, other @@ -169,7 +167,7 @@ with `x`. Before permanently deleting its conversation, assign another manager i for every active team it manages. There is no member Actions menu. Removing the reporting membership leaves that conversation independent; another manager is never assigned automatically. -`a` opens Add member. The picker uses `Name` and `Project` columns and does not display IDs. +`a` opens Add chat. The picker uses `Name` and `Project` columns and does not display IDs. ## Recent interactions: scrolling, expanding replies and opening their conversations @@ -215,7 +213,7 @@ no message box. To talk to the manager, click its card and type in its normal ch Selecting a team no longer opens or locks its manager's conversation. The overview can be read without resuming any member. Clicking a member uses the ordinary Chats door, which -says any refusal there. If a local manager's conversation is missing, `+ Manager` offers a +says any refusal there. If a local manager's conversation is missing, `+ AI manager` offers a new manager. Starting a manager opens its fresh conversation in Chats immediately. ## What does +4 idle mean on a team @@ -310,7 +308,7 @@ Teams owns its keyboard. Typing here does not edit a manager's draft. | `m` | Move into… | | `space` on a team | pick it for a move of several | | `p` | member list | -| `a` | Add member picker | +| `a` | Add chat picker | | `M` | start a manager when offered | | `d` | confirm permanent team deletion | | `u` | Undo a move while offered | @@ -385,7 +383,7 @@ it does not reopen disbanded teams. Over `--host` quiet-team suggestions are not The Global manager card offers its optional creation button before the first team exists. Below it, the page says what a team is in one sentence and offers two buttons: **`✦ Organize my conversations`**, which suggests teams from the conversations you have open, and -**`+ New team`**. `o` and `n` press them. +**`+ New AI team`**. `o` and `n` press them. ## Over --host: why a closed team's report is not readable @@ -422,9 +420,9 @@ The confirmation lists them. History stays read-only with Show closed; there is ## New team placement and removing a member -The Teams sidebar keeps `+ New team` directly after the displayed teams list. `Show closed` -remains at the bottom of the sidebar. `+ Add member` and `+ Add subteam` stay together -on the left of the selected team's header, followed by `Choose manager`; `Settings` and `Disband` sit on the right. +The Teams sidebar keeps `+ New AI team` directly after the displayed teams list. `Show closed` +remains at the bottom of the sidebar. `+ Add chat` and `+ Add subteam` stay together +on the left of the selected team's header, followed by `Choose AI manager`; `Settings` and `Disband` sit on the right. A member card's `x` opens a confirmation naming the member and team. `cancel` is selected by default; choose `yes` to remove only that team's membership. Its work finishes and its @@ -432,7 +430,7 @@ conversation remains. Removing the manager requires replacing it first. ## Create an empty team or select several conversations -`+ New team` opens a creation dialog over Teams, keeping the overview behind it. +`+ New AI team` opens a creation dialog over Teams, keeping the overview behind it. `+ Add subteam` uses the same dialog with the selected parent named in its title. Enter Name, choose Colour, and optionally select members from the searchable Name/Project list. Click several rows to toggle their checkboxes; filtering preserves hidden selections. `tab` @@ -441,7 +439,7 @@ cancels. No internal IDs are shown. A duplicate name or invalid parent is refuse You may create an empty team and add members afterward. Creation never assigns a manager or opens its members. Both routes land on the new team overview. A failed save is reported there. -Chats also offers `+ New team` in its full saved-conversation grid: select cards, then name +Chats also offers `+ New AI team` in its full saved-conversation grid: select cards, then name and create. The full conversation grid itself lives only in Chats. ## Keyboard hints in team menus @@ -449,21 +447,21 @@ and create. The full conversation grid itself lives only in Chats. Team menus show grey instructions together at the left of the box footer, with the key first: `up/down move · space select · esc cancel · enter create` in New team and Add subteam. Narrow boxes wrap the instructions rather than hiding them. Only text fields, -colour, and members are tab stops; the creation hint stays in that footer. Add member and -Choose manager show `up/down move · enter choose · esc cancel`; creating a new conversation +colour, and members are tab stops; the creation hint stays in that footer. Add chat and +Choose AI manager show `up/down move · enter choose · esc cancel`; creating a new conversation shows `enter create · esc cancel`. Settings, Move, Organize, and the Chats team-naming card use the same key-first format. Clickable action hints act on the same choices as their keys. -## Choose manager — change a team's manager from existing members +## Choose AI manager — change a team's manager from existing members -`Choose manager` in the selected team's header opens a filtered Name/Project list of its +`Choose AI manager` in the selected team's header opens a filtered Name/Project list of its existing members, excluding the current manager. `cancel` is selected by default. Choose a member with Enter or click; Esc cancels. Only this team's manager changes. The former manager remains a member, and both conversations keep their work and history. The picker cannot create a conversation or add membership. All teams also offers -`+ Add member` for adding a global-manager candidate; automatic memberships stay protected. For a new manager, first use -`+ Add member` to add or create its conversation, then use `Choose manager`. +`+ Add chat` for adding a global-manager candidate; automatic memberships stay protected. For a new manager, first use +`+ Add chat` to add or create its conversation, then use `Choose AI manager`. An ordinary team manager cannot be removed or permanently deleted while it manages an active team. The deletion confirmation names every active team it manages and says `Assign another manager before deleting it.` Replace it in every named @@ -476,7 +474,7 @@ manage an unrelated team. Managing sibling teams requires also managing their co ancestor. The global All teams manager is a separate conversation; it cannot also manage an ordinary team. Ordinary membership in multiple teams remains allowed. -Choose manager and team moves refuse changes that introduce conflicting responsibilities, +Choose AI manager and team moves refuse changes that introduce conflicting responsibilities, naming the affected teams. Replacing a parent manager can also be refused if it would leave the former manager managing sibling teams without a managed ancestor. The refusal names the subteam managers to assign first, before changing the parent manager. Older conflicting assignments remain visible and can be repaired @@ -489,7 +487,7 @@ missing, its card says `Conversation unavailable`; add or select a replacement e A blocked deletion names every active team the conversation manages. In a short terminal, scroll the message with the mouse wheel or Page Up/Page Down. Cancel/delete and the -keyboard hints remain visible. Choose replacements through Teams → Choose manager first. +keyboard hints remain visible. Choose replacements through Teams → Choose AI manager first. ## Global manager — create, delete and recreate @@ -510,8 +508,8 @@ manage an ordinary team; conflicting old assignments must be repaired before del Without a global manager, the card shows only `Optional · coordinates the managers of top-level teams` and `+ Global manager`. This same state returns after deletion and when the saved manager conversation is missing. With a manager, the creation button disappears; -`+ Add member` and `Settings` remain alongside the conversation's `x` delete control. -`Choose manager` is unavailable for the global role. `Settings` retains global spending controls. +`+ Add chat` and `Settings` remain alongside the conversation's `x` delete control. +`Choose AI manager` is unavailable for the global role. `Settings` retains global spending controls. ## Model names on manager and member cards diff --git a/internal/manual/chat/what-i-can-do.md b/internal/manual/chat/what-i-can-do.md index f409e0e286..974baa2558 100644 --- a/internal/manual/chat/what-i-can-do.md +++ b/internal/manual/chat/what-i-can-do.md @@ -206,7 +206,7 @@ change it.** If the foreground command is still running then, the same process is kept as a background job; the call returns its output so far and a job id, and the chat moves on. Nothing is killed or restarted. -The row is labelled **background after** on `/settings`' Safety tab; its key is +The row is labelled **background shell commands after** in `/settings` → Tasks; its key is `bash.background_after_seconds`. The session engine and the visible countdown arm from it together at launch, so a change applies to the **next session**. It is a machine brake, so the model's `change_setting` tool refuses to widen it; change it yourself in the panel. @@ -1000,7 +1000,7 @@ settings rather than keeping a launch-time copy. With `search.provider` on `auto`, an Exa key raises the next search to `exa`; a Firecrawl key keeps the provider named `firecrawl` and raises that provider's ceiling, so the visible change is `firecrawl · with your key`. The selected -**searching** row in `/settings` → Context states the current answer. With no +**web search provider** row in `/settings` → Connections states the current answer. With no keys it says `now firecrawl, keyless — set search.exaKey or search.firecrawlKey to raise it`. diff --git a/internal/manual/chat/what-i-remember.md b/internal/manual/chat/what-i-remember.md index 49f7d03e24..6dfb0cfbf7 100644 --- a/internal/manual/chat/what-i-remember.md +++ b/internal/manual/chat/what-i-remember.md @@ -589,7 +589,7 @@ quarantine — grouped by how far each one's truth reaches, and answers with at true now in its place. **What it costs.** One call on the **small work** class — the `consolidate` role -in `/settings` → Providers — a few times a day at most. It spends under the same +in `/settings` → Models — a few times a day at most. It spends under the same daily budget as everything else that runs in the background, and it is the first thing a spent day stops paying for. @@ -710,7 +710,7 @@ reading as three times the price of its neighbours. `/cost` also names how many of those paid requests were empty at their ceiling. You can point that row at a different model — the **reflex** row in -`/settings` → Providers — or pin the +`/settings` → Models — or pin the `reflex` role by itself under `pinned roles`. A change is live: the next turn's pair uses it. diff --git a/internal/manual/chat/worker-harness.md b/internal/manual/chat/worker-harness.md index 3624ff0ee5..d719bf6b25 100644 --- a/internal/manual/chat/worker-harness.md +++ b/internal/manual/chat/worker-harness.md @@ -159,7 +159,7 @@ or the model; the task's room does, where the store's spend has them. The parts row each, the finished ones included. Every one of those rows is a door: click the run's row, or select it and press `enter`, and its room opens over the conversation; click a part's row or a check's row and THAT task's room opens. It is the -room the sessions place opens and the run's tab opens: what the task was asked, its steps, +room the activity page opens and the run's tab opens: what the task was asked, its steps, its notes, and the box that leaves a note. A task handed to a program such as senior-dev opens its own program room instead (see *A program's task page is a conversation, not steps*). `esc` goes back to the conversation exactly as @@ -283,7 +283,7 @@ to the column's width; the glyph and the `$` are never spent on it. **The line is there only while a step is in flight.** A task that has not started, one held behind named work, and one that has landed all draw their ordinary row and no live line — the store clears the step the moment its command ends. These rows are a run's **plan rows**, drawn -in the sessions place (`/history`, `ctrl+.`, `alt+4`, and the roster raised over the frame), not +in the activity page (`/history`, `ctrl+.`, `alt+4`, and the roster raised over the frame), not on the always-on column, which draws this conversation's own tree. ## What a run task's room shows while it runs @@ -332,7 +332,7 @@ messages — say it to main`, or ` reads no more messages (…)` with i `enter` over a sentence says the same line and leaves your words in the box. Once the run has ended its foot and its box say `this task has finished — say it to main`. -In the sessions place, `enter` on the program's row opens the same page as a page of that +In the activity page, `enter` on the program's row opens the same page as a page of that place, with no box at all. ## Reading a program's actions — the step words down the side, how each came out, the call in flight, how long it has run @@ -367,7 +367,7 @@ once more after its work has landed, so the note on where the work went is on th ## A program's raw calls — ctrl+y, the dialogue with its model, what it sent and what the model answered -`ctrl+y` on a program's page — in its room or in the sessions place — turns it to the raw +`ctrl+y` on a program's page — in its room or in the activity page — turns it to the raw calls the program made, and `ctrl+y` again turns it back to the actions; the key row says which: `ctrl+y calls` or `ctrl+y actions`. A page opens on the actions. @@ -468,7 +468,7 @@ this conversation's plan**, so a task another chat spawned is never reachable: - **pause** / **resume** — hold a task and everything under it out of the ready frontier without changing its rung, so running steps finish and nothing new in the subtree is launched; or release the hold. The key is `p`: a running row - reads `p pause` and a held one `p resume`. The key is on the sessions place's + reads `p pause` and a held one `p resume`. The key is on the activity page's rows only: in a task's room `p` is a letter in the box. A run cannot be paused as a whole, so its own row names no `p pause`. - **cancel** — end a task, its descendants and the work hard-depending on it. The diff --git a/internal/manual/chatdisplaytruth_test.go b/internal/manual/chatdisplaytruth_test.go index 7c0f8f490d..fce1c6b799 100644 --- a/internal/manual/chatdisplaytruth_test.go +++ b/internal/manual/chatdisplaytruth_test.go @@ -97,8 +97,8 @@ func TestManualStatesHelpSettingsGreetingAndManagerDisplayTruth(t *testing.T) { }{ {"commands", "codeaf --help — asking one command what it takes, which is not a failure", []string{"run `codeaf --help` for every command, `codeaf help env` for the variables."}}, {"commands", "codeaf --help, and --help on any command — what does this command take, what are its flags, how do I see the usage", []string{"`codeaf help env`", "every variable and its default"}}, - {"commands", "The ten settings tabs", []string{"Session · Context · Workspace · Display · Spending · Safety · Tasks · Teams · Providers · Connections"}}, - {"models-and-cost", "Where are the spending limits — the Spending tab, and every door onto it", []string{"Tasks · Teams · Providers"}}, + {"commands", "Settings categories — where did Session, Context, Workspace, Display and Providers go", []string{"General · Models · Memory · Tasks · AI teams · Permissions · Spending · Connections · Privacy", "Old saved keys and values are unchanged"}}, + {"models-and-cost", "Where are the spending limits — the Spending tab, and every door onto it", []string{"Tasks · AI teams · Permissions"}}, {"getting-started", "Getting started — first time setup, what happens the first time I run codeaf", []string{"nothing elsewhere", "What would you like to work on?", "home", "`--no-host`"}}, {"team-manager", "Making a manager", []string{"team's name", "`@second`", "`@lead`", "All teams"}}, } { diff --git a/internal/manual/truth_test.go b/internal/manual/truth_test.go index da3a8ce197..e18ae2e45f 100644 --- a/internal/manual/truth_test.go +++ b/internal/manual/truth_test.go @@ -349,7 +349,7 @@ func quotedFacts(t *testing.T) []quotedFact { }, { fact: "what practice may take of the day", owner: "config.DefaultPracticeBudgetUSD", value: dollarsOwed(config.DefaultPracticeBudgetUSD), - quotes: []quotedIn{{"models-and-cost", "| **practice** | `$%s of the day` |"}}, + quotes: []quotedIn{{"models-and-cost", "The separate resident’s practice default is `$%s of the day`"}}, }, { // The pass, which the page states in words because a person asking how // often their watch checks is not asking for a duration. diff --git a/internal/session/tools_settings.go b/internal/session/tools_settings.go index 857a281624..d6449be1bc 100644 --- a/internal/session/tools_settings.go +++ b/internal/session/tools_settings.go @@ -83,7 +83,7 @@ const changeSettingDescription = "Change one codeaf setting permanently: the set const settingsSchemaJSON = `{"type":"object","properties":{"key":{"type":"string","description":"One registry key, read in full"},"search":{"type":"string","description":"Filter the listing by key, label or hint"}},"additionalProperties":false}` -const changeSettingSchemaJSON = `{"type":"object","properties":{"key":{"type":"string","description":"The row's exact registry key"},"value":{"type":"string","description":"New value as text; empty clears it to the default"}},"required":["key","value"],"additionalProperties":false}` +const changeSettingSchemaJSON = `{"type":"object","properties":{"key":{"type":"string","description":"The row's exact registry key"},"value":{"type":"string","description":"Raw value accepted by settings, not a display label; empty clears it to the default"}},"required":["key","value"],"additionalProperties":false}` // settingsTools is the pair, or nothing at all inside a task node. // @@ -202,6 +202,9 @@ func (a *Agent) changeSettingTool(registry *config.Settings) bare.Tool { if refusal := modelSlotRefusal(row); refusal != "" { return refusal, true, nil } + if row.ChatPresentation().Hidden { + return fmt.Sprintf("%q (%s) is unavailable in chat settings; this legacy or resident-only control cannot be changed from chat.", settingLabel(row), key), true, nil + } before := row.Value() if err := row.Apply(parsed.Value); err != nil { // The registry's own wording, verbatim. It is the sentence the @@ -236,13 +239,18 @@ func (a *Agent) settingChanged(registry *config.Settings, key, before string) st return "changed " + key } after := row.Value() + label := settingLabel(row) + timing := toolSettingActivation(row) + if timing != "" { + timing = " " + strings.TrimSuffix(timing, ".") + "." + } + warning := projectOverrideWarning(a.config.Workspace, key) if after == before { - a.noteSetting(fmt.Sprintf("settings · %s is already %s", row.Label, settingReading(after))) - return fmt.Sprintf("%s (%s) was already %s; nothing changed.", row.Label, key, settingReading(after)) + a.noteSetting(fmt.Sprintf("settings · %s is already %s", label, settingReading(config.ChatChoiceLabel(row.Key, after, after)))) + return fmt.Sprintf("%s (%s) was already saved as %s; nothing changed.%s%s", label, key, settingDisplayValue(row, after), timing, warning) } - a.noteSetting(fmt.Sprintf("settings · %s · %s → %s", row.Label, settingReading(before), settingReading(after))) - return fmt.Sprintf("%s (%s) is now %s, saved to the profile — it will still be set the next time codeaf starts. It was %s.%s", - row.Label, key, settingReading(after), settingReading(before), projectOverrideWarning(a.config.Workspace, key)) + a.noteSetting(fmt.Sprintf("settings · %s · %s → %s · saved%s", label, settingReading(config.ChatChoiceLabel(row.Key, before, before)), settingReading(config.ChatChoiceLabel(row.Key, after, after)), timing)) + return fmt.Sprintf("%s (%s) saved to the profile as %s. It was %s.%s%s", label, key, settingDisplayValue(row, after), settingDisplayValue(row, before), timing, warning) } // settingReading is one value as a sentence can carry it. THE EMPTINESS LAW: a @@ -316,7 +324,7 @@ func modelSlotRefusal(row config.Setting) string { return "" } if slot.Slot == conversationSlot { - return fmt.Sprintf("%q (%s) is the model this conversation is running on, which is not a row in the profile: change it with /model, or on the Providers tab of /settings.", row.Label, row.Key) + return fmt.Sprintf("%q (%s) is the model this conversation is running on, which is not a row in the profile: change it with /model, or under Models in /settings.", row.Label, row.Key) } return fmt.Sprintf("%q (%s) is a binding the running session holds rather than a value in the profile, so neither this hand nor the /settings panel can write it. To send codeaf's own auxiliary calls to a particular model, set %q (%s) or %q (%s), or pin one role in %q (%s).", row.Label, row.Key, @@ -337,27 +345,28 @@ func modelSlotRefusal(row config.Setting) string { // word the person would use, and what the row says right now. func settingListing(registry *config.Settings, search string) string { needle := strings.ToLower(strings.TrimSpace(search)) - var out strings.Builder + groups := map[string][]string{} matched := 0 - for _, group := range registry.Groups() { - var lines []string - for _, row := range group.Rows { - if needle != "" && !settingMatches(row, needle) { - continue - } - lines = append(lines, " "+settingLine(row)) + for _, row := range registry.Rows() { + p := row.ChatPresentation() + if p.Hidden || (needle != "" && !settingMatches(row, needle)) { + continue } - if len(lines) == 0 { + groups[p.Category] = append(groups[p.Category], " "+settingLine(row)) + matched++ + } + if matched == 0 { + return fmt.Sprintf("No setting mentions %q. Call %s with no arguments to see every row.", search, settingsToolName) + } + var out strings.Builder + for _, category := range []string{"General", "Models", "Memory", "Tasks", "AI teams", "Permissions", "Spending", "Connections", "Privacy"} { + if len(groups[category]) == 0 { continue } - matched += len(lines) if out.Len() > 0 { out.WriteString("\n") } - out.WriteString(group.Title + "\n" + strings.Join(lines, "\n") + "\n") - } - if matched == 0 { - return fmt.Sprintf("No setting mentions %q. Call %s with no arguments to see every row.", search, settingsToolName) + out.WriteString(category + "\n" + strings.Join(groups[category], "\n") + "\n") } head := fmt.Sprintf("%d settings, as they read now. Change one with %s, naming the key on the left.\n\n", matched, changeSettingToolName) if needle != "" { @@ -370,9 +379,9 @@ func settingListing(registry *config.Settings, search string) string { // true about who may write it. Empty parts are dropped rather than printed as // separators with nothing between them — the emptiness law, one line at a time. func settingLine(row config.Setting) string { - parts := []string{row.Key, row.Label} + parts := []string{row.Key, settingLabel(row)} if value := strings.TrimSpace(row.Value()); value != "" { - parts = append(parts, value) + parts = append(parts, settingDisplayValue(row, value)) } if name, pinned := row.PinnedBy(); pinned { parts = append(parts, "set by "+name) @@ -390,13 +399,22 @@ func settingLine(row config.Setting) string { // this session may write it. func settingDetail(row config.Setting) string { var out strings.Builder - fmt.Fprintf(&out, "%s · %s · %s\n", row.Label, row.Key, row.Category) - reading := settingReading(row.Value()) + p := row.ChatPresentation() + fmt.Fprintf(&out, "%s · %s · %s\n", settingLabel(row), row.Key, p.Category) + reading := settingDisplayValue(row, row.Value()) if receipt := row.Receipt(); receipt != "" { reading += " · " + receipt } fmt.Fprintf(&out, "now: %s\ntakes: %s\n", reading, row.Accepts()) - if hint := strings.TrimSpace(row.Hint); hint != "" { + out.WriteString(settingOptions(row)) + fmt.Fprintf(&out, "scope: %s\n", p.Scope) + if activation := toolSettingActivation(row); activation != "" { + out.WriteString(activation + "\n") + } + if p.Hidden { + out.WriteString("Unavailable in chat settings; legacy or resident-only control.\n") + } + if hint := strings.TrimSpace(settingHint(row)); hint != "" { out.WriteString(hint + "\n") } if name, pinned := row.PinnedBy(); pinned { @@ -408,6 +426,8 @@ func settingDetail(row config.Setting) string { out.WriteString(row.SelfServiceRefusal() + "\n") case modelSlotRefusal(row) != "": out.WriteString(modelSlotRefusal(row) + "\n") + case p.Hidden: + out.WriteString("This control cannot be changed from chat.\n") default: fmt.Fprintf(&out, "I can change this one with %s.\n", changeSettingToolName) } @@ -418,7 +438,8 @@ func settingDetail(row config.Setting) string { // search box matches on (internal/tui3's settings.go), so a word that finds a // row in the panel finds it here. func settingMatches(row config.Setting, needle string) bool { - haystack := strings.ToLower(row.Key + " " + row.Label + " " + row.Hint + " " + row.Category) + p := row.ChatPresentation() + haystack := strings.ToLower(strings.Join(append([]string{row.Key, row.Label, row.Hint, row.Category, p.Label, p.Category, p.Description, row.Value(), settingDisplayValue(row, row.Value())}, p.Aliases...), " ")) return strings.Contains(haystack, needle) } @@ -466,7 +487,10 @@ func nearSettingKeys(rows []config.Setting, asked string) []string { var hits []hit for _, row := range rows { key := strings.ToLower(row.Key) - haystack := key + " " + strings.ToLower(row.Label) + if row.ChatPresentation().Hidden { + continue + } + haystack := key + " " + strings.ToLower(row.Label+" "+settingLabel(row)) score := 0 for _, word := range words { if strings.Contains(haystack, word) { diff --git a/internal/session/tools_settings_presentation.go b/internal/session/tools_settings_presentation.go new file mode 100644 index 0000000000..b13c69ab7c --- /dev/null +++ b/internal/session/tools_settings_presentation.go @@ -0,0 +1,56 @@ +package session + +import ( + "fmt" + "github.com/Agent-Field/codeaf/internal/config" + "strings" +) + +func settingLabel(row config.Setting) string { + if label := row.ChatPresentation().Label; label != "" { + return label + } + return row.Label +} +func settingHint(row config.Setting) string { + if hint := row.ChatPresentation().Description; hint != "" { + return hint + } + return row.Hint +} + +// Expose both vocabularies: the displayed option is never silently substituted +// for the raw argument accepted by the long-lived tool API. +func settingDisplayValue(row config.Setting, raw string) string { + label := config.ChatChoiceLabel(row.Key, raw, raw) + if label != raw { + return fmt.Sprintf("%s (raw value: %s)", settingReading(label), settingReading(raw)) + } + return settingReading(raw) +} +func settingOptions(row config.Setting) string { + values := row.Choices + if row.Kind == config.SettingBool { + values = []string{"on", "off"} + } + if len(values) == 0 { + return "" + } + var out strings.Builder + out.WriteString("Write the raw value, not the display label. Options:\n") + for _, raw := range values { + fmt.Fprintf(&out, " %q => %s: %s\n", raw, settingLabel(row), config.ChatChoiceLabel(row.Key, raw, raw)) + } + return out.String() +} + +// toolSettingActivation describes this writer, which saves the profile without +// calling the TUI's live transport seams. +func toolSettingActivation(row config.Setting) string { + switch row.Key { + case config.KeyRouting, config.KeyLaneGuard, config.LaneSettingKey(config.LaneSlotTalk), config.LaneBorrowKey(config.LaneSlotTalk): + return "Restart the CLI to ensure already-open chats use this change." + default: + return row.ChatPresentation().Activation + } +} diff --git a/internal/session/tools_settings_presentation_test.go b/internal/session/tools_settings_presentation_test.go new file mode 100644 index 0000000000..56188b7912 --- /dev/null +++ b/internal/session/tools_settings_presentation_test.go @@ -0,0 +1,176 @@ +package session + +import ( + "encoding/json" + "github.com/Agent-Field/codeaf/internal/config" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestChatSettingsPresentationDiscoveryAndCompatibility(t *testing.T) { + agent, _ := settingsAgent(t) + read, _ := settingsHands(t, agent) + for _, tc := range []struct{ search, key string }{ + {"completed tool details", config.KeyWork}, {"show hints", config.KeyHints}, + {"Privacy", config.KeyHistoryEnabled}, {"concurrent tasks", config.KeyTaskParallel}, + {"disable hints", config.KeyHints}, {"remember across chats", config.KeyMemoryEnabled}, + } { + text, bad := callSetting(t, read, map[string]string{"search": tc.search}) + if bad || !strings.Contains(text, tc.key) { + t.Fatalf("%q did not discover %s: %s", tc.search, tc.key, text) + } + } + text, _ := callSetting(t, read, map[string]string{}) + for _, key := range []string{config.KeyPracticeIdle, config.KeyPracticeBudget, config.KeyBriefAfter, config.KeyTenureAfter, config.KeySplitPct} { + if strings.Contains(text, key+" ·") { + t.Fatalf("resident-only %s leaked into chat list", key) + } + } +} + +func TestChatSettingsReadWriteReadKeepsRawSemantics(t *testing.T) { + agent, profile := settingsAgent(t) + read, write := settingsHands(t, agent) + hints, _ := callSetting(t, read, map[string]string{"key": config.KeyHints}) + for _, want := range []string{`"on" => show hints: off`, `"off" => show hints: on`} { + if !strings.Contains(hints, want) { + t.Fatalf("missing inverse hints mapping %s: %s", want, hints) + } + } + for _, tc := range []struct{ key, raw, display string }{ + {config.KeyHints, "on", "off"}, {config.KeyHints, "off", "on"}, + {config.KeyWork, "open", "expanded"}, {config.KeyWork, "fold", "collapsed"}, + {config.KeyMemoryEnabled, "off", "off"}, {config.KeyMemoryEnabled, "on", "on"}, + } { + receipt, bad := callSetting(t, write, map[string]string{"key": tc.key, "value": tc.raw}) + if bad || !strings.Contains(receipt, "saved") { + t.Fatalf("write %s failed: %s", tc.key, receipt) + } + detail, bad := callSetting(t, read, map[string]string{"key": tc.key}) + if bad || !strings.Contains(detail, "now: "+tc.display) { + t.Fatalf("readback %s: %s", tc.key, detail) + } + row, _ := config.NewSettings(config.SettingsOptions{ProfileDir: profile}).Row(tc.key) + if row.Value() != tc.raw { + t.Fatalf("registry raw semantics changed: %s = %s", tc.key, row.Value()) + } + if tc.key == config.KeyHints && config.HintsAt(profile) != (tc.raw == "off") { + t.Fatal("hints inversion changed") + } + if tc.key == config.KeyMemoryEnabled && !strings.Contains(receipt, "Restart the CLI") { + t.Fatalf("memory falsely implies immediate activation: %s", receipt) + } + } + values := profileJSON(t, profile) + if values[config.KeyHints] != true || values[config.KeyWork] != "fold" || values[config.KeyMemoryEnabled] != "on" { + t.Fatalf("wrong persisted values: %v", values) + } +} + +func TestChatSettingsRefusalsPreserveProfile(t *testing.T) { + agent, profile := settingsAgent(t) + read, write := settingsHands(t, agent) + callSetting(t, write, map[string]string{"key": config.KeyWork, "value": "open"}) + before, _ := json.Marshal(profileJSON(t, profile)) + for _, tc := range []struct{ key, value string }{ + {config.KeyWork, "expanded"}, // readable labels do not redefine the old API + {config.KeyMemoryEnabled, "sometimes"}, + {config.KeyToolApprovalMode, "allow"}, + {config.KeyTaskParallel, "100"}, + {config.KeyPracticeIdle, "on"}, + } { + text, bad := callSetting(t, write, map[string]string{"key": tc.key, "value": tc.value}) + if !bad { + t.Fatalf("invalid/guarded %s accepted: %s", tc.key, text) + } + after, _ := json.Marshal(profileJSON(t, profile)) + if string(before) != string(after) { + t.Fatalf("refusal for %s changed profile", tc.key) + } + } + detail, bad := callSetting(t, read, map[string]string{"key": config.KeyPracticeIdle}) + if bad || !strings.Contains(detail, "Unavailable in chat settings") || strings.Contains(detail, "I can change this one") { + t.Fatalf("hidden exact-key detail misleading: %s", detail) + } +} + +func TestChatSettingsReceiptNamesRestartAndProjectOverride(t *testing.T) { + agent, profile := settingsAgent(t) + path := config.ProjectConfigPath(agent.config.Workspace) + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(map[string]any{config.KeyHistoryEnabled: true}) + if err := os.WriteFile(path, raw, 0600); err != nil { + t.Fatal(err) + } + _, write := settingsHands(t, agent) + receipt, bad := callSetting(t, write, map[string]string{"key": config.KeyHistoryEnabled, "value": "off"}) + if bad || !strings.Contains(receipt, "Restart the CLI") || !strings.Contains(receipt, "outranks the profile") || !strings.Contains(receipt, path) { + t.Fatalf("receipt omits actual scope/timing: %s", receipt) + } + if config.HistoryEnabledAt(profile) { + t.Fatal("profile value did not save") + } +} + +func TestChatSettingsHintNoticeUsesVisibleMeaning(t *testing.T) { + agent, _ := settingsAgent(t) + agent.hub = newEventHub() + events := agent.hub.subscribe() + _, write := settingsHands(t, agent) + receipt, bad := callSetting(t, write, map[string]string{"key": config.KeyHints, "value": "on"}) + if bad { + t.Fatal(receipt) + } + notice := waitForNotice(t, events) + if !strings.Contains(notice, "show hints · on → off") || strings.Contains(notice, "raw value") { + t.Fatalf("user notice exposes raw mapping or wrong direction: %s", notice) + } + if !strings.Contains(receipt, "raw value: on") { + t.Fatalf("model receipt lost compatibility mapping: %s", receipt) + } +} + +func TestChatSettingsTransportChangesNameTheirActivation(t *testing.T) { + agent, profile := settingsAgent(t) + read, write := settingsHands(t, agent) + const timing = "Restart the CLI to ensure already-open chats use this change." + for _, tc := range []struct{ key, value string }{ + {config.KeyRouting, "price"}, + {config.KeyLaneGuard, "off"}, + {config.LaneSettingKey(config.LaneSlotTalk), "pinned: cloudflare, borrow when slow"}, + } { + detail, bad := callSetting(t, read, map[string]string{"key": tc.key}) + if bad || !strings.Contains(detail, timing) { + t.Fatalf("read %s omits tool-specific activation: %s", tc.key, detail) + } + for n := 0; n < 2; n++ { + receipt, bad := callSetting(t, write, map[string]string{"key": tc.key, "value": tc.value}) + if bad || !strings.Contains(receipt, timing) || !strings.Contains(receipt, "saved") { + t.Fatalf("write %s omits scope/timing: %s", tc.key, receipt) + } + } + } + values := profileJSON(t, profile) + if values[config.KeyRouting] != "price" || values[config.KeyLaneGuard] != false || values[config.LaneBorrowKey(config.LaneSlotTalk)] != true || values[config.LaneSettingKey(config.LaneSlotTalk)] != "cloudflare" { + t.Fatalf("transport values not persisted: %v", values) + } + // Borrow is a persisted companion field, not a separately exposed registry row. + // It stays unavailable by exact key; the composite lane choice above owns it. + borrow := config.LaneBorrowKey(config.LaneSlotTalk) + if detail, bad := callSetting(t, read, map[string]string{"key": borrow}); !bad { + t.Fatalf("invented borrow row became readable: %s", detail) + } + if receipt, bad := callSetting(t, write, map[string]string{"key": borrow, "value": "off"}); !bad { + t.Fatalf("invented borrow row became writable: %s", receipt) + } + if !config.LaneBorrowAt(profile, config.LaneSlotTalk) { + t.Fatal("refused companion write altered borrow") + } + if toolSettingActivation(config.Setting{Key: borrow}) != timing { + t.Fatal("borrow timing differs if exposed later") + } +} diff --git a/internal/session/tools_settings_test.go b/internal/session/tools_settings_test.go index 1e162306b5..ddd6c36b29 100644 --- a/internal/session/tools_settings_test.go +++ b/internal/session/tools_settings_test.go @@ -105,22 +105,25 @@ func TestSettingsListsEveryRowByItsRegistryKey(t *testing.T) { // The conversation's own row reads the model this session is actually on. // Without the live seam it reads blank, and a sheet whose first row is empty // is a sheet that looks broken. - if !strings.Contains(text, config.ModelSettingKey("talk")+" · conversation · anthropic/claude-opus-5") { + if !strings.Contains(text, config.ModelSettingKey("talk")+" · chat model · anthropic/claude-opus-5") { t.Errorf("the conversation row does not name the model this session is on:\n%s", text) } registry := config.NewSettings(config.SettingsOptions{ProfileDir: profile}) for _, row := range registry.Rows() { + if row.ChatPresentation().Hidden { + continue + } if !strings.Contains(text, row.Key) { t.Errorf("the listing does not carry the row %s", row.Key) } } - for _, category := range config.SettingCategories { + for _, category := range []string{"General", "Models", "Memory", "Tasks", "AI teams", "Permissions", "Spending", "Connections", "Privacy"} { if !strings.Contains(text, category) { t.Errorf("the listing does not name the %q group", category) } } // And the marker that stops a model spending a call to find out. - if !strings.Contains(text, config.KeyDailyBudget+" · daily budget") { + if !strings.Contains(text, config.KeyDailyBudget+" · daily spending limit") { t.Errorf("the listing does not read as `key · label · value`:\n%s", text) } if !strings.Contains(text, "yours to change") { diff --git a/internal/tui3/addrow_test.go b/internal/tui3/addrow_test.go index b0a6156498..36ce7a9291 100644 --- a/internal/tui3/addrow_test.go +++ b/internal/tui3/addrow_test.go @@ -98,7 +98,7 @@ func TestProvidersTabShowsTheAddRowBeforeAnyCustomConnectionExists(t *testing.T) a.prepareModelServices() a.raiseSettings() for i, tab := range settingTabs { - if tab == tabProviders { + if tab == tabConnections { a.sheet.tab = i } } diff --git a/internal/tui3/app.go b/internal/tui3/app.go index 3f1a28518d..f1ff7aa811 100644 --- a/internal/tui3/app.go +++ b/internal/tui3/app.go @@ -4173,6 +4173,17 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) { a.touch() return a, nil } + if a.at(pageSettings) && a.sheet.edit != nil { + return a, nil + } + // A settings choice owns scrolling while its options are open. + if a.at(pageSettings) && a.sheet.choice != nil { + c := a.sheet.choice + c.cursor = max(0, min(len(c.item.row.Choices)-1, c.cursor+placeWheelDelta(msg.Mouse().Button))) + a.sheet.savedKey = "" + a.touch() + return a, nil + } // A SETTINGS ROW'S MODEL LIST ANSWERS BEFORE THE PAGE, including // its nav: the row being chosen must move, not the slot under it. if a.at(pageSettings) && a.sheet.sel != nil && a.sheet.sel.pick.open { @@ -4945,7 +4956,7 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) { return a, nil } if a.at(pageSettings) { - a.sheetHover(msg.Mouse().Y) + a.sheetHoverAt(msg.Mouse().X, msg.Mouse().Y) return a, nil } if a.at(pageTasks) { @@ -6569,13 +6580,7 @@ func (a *app) settle() tea.Cmd { a.turnBegan, a.turnOutStart, a.turnCostAt = time.Time{}, 0, 0 a.col.open() a.approval = a.approvalPosture() - a.mouse = config.MouseEnabledAt(a.profileDir) - a.timestamps = config.TimestampsAt(a.profileDir) - a.workMode = config.WorkAt(a.profileDir) - a.adoptIcons() - a.hopQuick = config.QuickSwitchAt(a.profileDir) - a.askWait = a.consentWait() - a.notices.enabled = config.HintsAt(a.profileDir) + a.refreshProfileUI() // AND THE ANSWER HAS JUST ARRIVED, which is when somebody starts reading // it: the conversation's tip waits its quiet out from here (notice.go). a.stirred() @@ -9219,6 +9224,9 @@ func (a *app) paste(text string) tea.Cmd { if a.at(pageSettings) { flat := strings.ReplaceAll(text, "\n", " ") switch { + case a.sheet.choice != nil: + // Closed choices have no text field; do not alter the hidden search. + return nil case a.sheet.edit != nil: a.sheet.edit.box.insert(flat) case a.sheet.sel != nil: diff --git a/internal/tui3/budget.go b/internal/tui3/budget.go index fd60a08af3..335d404e53 100644 --- a/internal/tui3/budget.go +++ b/internal/tui3/budget.go @@ -37,7 +37,6 @@ var budgetRows = []struct { {[]string{"day", "daily", "today"}, config.KeyDailyBudget}, {[]string{"conversation", "chat", "session"}, config.KeySpendRail}, {[]string{"plan", "plans", "ask"}, config.KeyPlanConsent}, - {[]string{"practice"}, config.KeyPracticeBudget}, } // budgetRowFor reads the first word of an argument as a row name. @@ -73,6 +72,10 @@ func budgetWords() string { // /budget plan the tab, on that row func (a *app) budget(rest string) tea.Cmd { rest = strings.TrimSpace(rest) + if word, _, _ := strings.Cut(rest, " "); strings.EqualFold(word, "practice") { + a.note("Practice limits are not used by chats. Use /budget day, /budget conversation or /budget plan.") + return nil + } if rest == "" { a.noticeEvent(eventBudgetShown) return a.openSpending(config.KeyDailyBudget) diff --git a/internal/tui3/caretpark_test.go b/internal/tui3/caretpark_test.go index 01af3db90a..f18ca89b5b 100644 --- a/internal/tui3/caretpark_test.go +++ b/internal/tui3/caretpark_test.go @@ -88,8 +88,10 @@ func TestTheSearchBoxParksItsCaretAfterTheQuery(t *testing.T) { t.Fatalf("the caret stands at %d, after %q, want immediately after %q", caretX, before, "provide") } - if !strings.Contains(strings.Join(plainLines, "\n"), "saved to") { - t.Fatal("the settings frame lost its saved-profile note") + if item, ok := a.sheet.current(); ok && item.row.Key != "" { + if scope := item.row.ChatPresentation().Scope; scope != "" && !strings.Contains(strings.Join(plainLines, "\n"), scope) { + t.Fatalf("the selected setting lost its scope: %q", scope) + } } } @@ -102,7 +104,7 @@ func TestTheSearchBoxRestsItsCaretOnTheSameRow(t *testing.T) { a.raiseSettings() a.sheet.build() lines, _, caretX, caretY, _ := a.placeFrameNow(90, 30) - boxRow, plainLines := caretRowOf(t, lines, "›", "type to search") + boxRow, plainLines := caretRowOf(t, lines, "›", "Search all settings") if caretY != boxRow { t.Fatalf("the resting caret is on row %d, want the box's row %d:\n%s", caretY, boxRow, strings.Join(plainLines, "\n")) @@ -164,7 +166,7 @@ func TestTheConnectionsEntryParksItsCaretInTheBox(t *testing.T) { // AND NOT IN THE RESTING SEARCH BOX AT THE FOOT, which is where it stood // before the sheet learned to answer for its own row's box. for i, line := range plainLines { - if strings.Contains(line, "type to search") && i == caretY { + if strings.Contains(line, "Search all settings") && i == caretY { t.Fatalf("the caret is parked in the resting search box at row %d", i) } } diff --git a/internal/tui3/chrome_test.go b/internal/tui3/chrome_test.go index f6006f07b0..5ae711a5bf 100644 --- a/internal/tui3/chrome_test.go +++ b/internal/tui3/chrome_test.go @@ -84,6 +84,11 @@ func sheetHas(a *app, want string) bool { func cursorTo(t *testing.T, a *app, key string) { t.Helper() for tab := range settingTabs { + if a.sheet.advanced == nil { + a.sheet.advanced = map[string]bool{} + } + a.sheet.advanced[settingTabs[tab]] = true + a.sheet.build() if a.sheet.tab != tab { a.sheet.tab = tab a.sheet.cursor, a.sheet.top = 0, 0 @@ -108,6 +113,9 @@ func cursorTo(t *testing.T, a *app, key string) { func TestEverySettingRowHasATab(t *testing.T) { registry := config.NewSettings(config.SettingsOptions{ProfileDir: t.TempDir()}) for _, row := range registry.Rows() { + if row.ChatPresentation().Hidden { + continue + } meta, ok := settingMetaFor(row) if !ok { t.Fatalf("registry row %q has no tab — add one line to settingUI", row.Key) @@ -148,7 +156,7 @@ func TestTheSettingsPanelOpensOnBothDoorsAndClosesOnEsc(t *testing.T) { if strings.Contains(plain(frame(a)), microcopy) { t.Fatal("the panel is drawn over a frame that is still showing its status line") } - if !sheetHas(a, "memory") { + if !sheetHas(a, "General") { t.Fatalf("the Session tab is missing its first row:\n%s", strings.Join(sheetLabels(a), "\n")) } } @@ -157,67 +165,24 @@ func TestTheSettingsPanelOpensOnBothDoorsAndClosesOnEsc(t *testing.T) { func TestTheSettingsTabsSwitchAndCarryTheirOwnRows(t *testing.T) { a, _ := sheetApp(t) a.openSettings() - - if got := settingTabs[a.sheet.tab]; got != tabSession { - t.Fatalf("the panel opened on %q, want %q", got, tabSession) + if got := settingTabs[a.sheet.tab]; got != tabGeneral { + t.Fatalf("opened on %q", got) } - for _, want := range []string{"memory", "fallback models"} { - if !sheetHas(a, want) { - t.Fatalf("the Session tab is missing %q:\n%s", want, strings.Join(sheetLabels(a), "\n")) - } - } - if sheetHas(a, "compact at") { - t.Fatal("a Context row is showing on the Session tab") - } - // AND THE FOUR QUESTIONS THAT LEFT IT ARE GONE FROM IT. The gate belongs to - // Safety, the task rows to Tasks, the money to Spending - // (docs/design/spending/DESIGN.md's information hierarchy) — and the ssh - // link to Workspace, because it lands next launch and is a fact about this - // machine rather than about the conversation in front of the reader. - for _, gone := range []string{"ask before running", "per conversation", "tasks at once", "ssh reuse"} { - if sheetHas(a, gone) { - t.Fatalf("%q is still on the Session tab:\n%s", gone, strings.Join(sheetLabels(a), "\n")) + for _, want := range settingTabs[1:] { + drive(t, a, key("right")) + if got := settingTabs[a.sheet.tab]; got != want { + t.Fatalf("landed %q want %q", got, want) } } - // THE CREW IS ON PROVIDERS, with the model it answers under — one tab, one - // question (settings.go's [modelsSection] says why it moved). - if sheetHas(a, "small work") { - t.Fatal("a crew row is showing on the Session tab") - } - drive(t, a, key("right")) - if got := settingTabs[a.sheet.tab]; got != tabContext { - t.Fatalf("→ landed on %q, want %q", got, tabContext) - } - if !sheetHas(a, "compact at") || sheetHas(a, "ask before running") { - t.Fatalf("the Context tab did not replace the Session rows:\n%s", - strings.Join(sheetLabels(a), "\n")) + if settingTabs[a.sheet.tab] != tabPrivacy { + t.Fatal("right must clamp") } - - // The walk clamps at both ends rather than wrapping — the rule every list - // on this surface follows (palette.go). - drive(t, a, key("left"), key("left"), key("left")) - if got := settingTabs[a.sheet.tab]; got != tabSession { - t.Fatalf("← past the first tab landed on %q", got) - } - // AND THE SSH ROWS ARE ON WORKSPACE, the tab about what this machine reaches - // on your behalf, two steps right of the one they used to be on. - drive(t, a, key("right"), key("right")) - if got := settingTabs[a.sheet.tab]; got != tabWorkspace { - t.Fatalf("→ landed on %q, want %q", got, tabWorkspace) + for range settingTabs { + drive(t, a, key("left")) } - for _, want := range []string{"ssh reuse", "ssh heartbeat", "ssh missed heartbeats", "ssh traffic"} { - if !sheetHas(a, want) { - t.Fatalf("the Workspace tab is missing %q:\n%s", want, strings.Join(sheetLabels(a), "\n")) - } - } - for i := 0; i < len(settingTabs)+3; i++ { - drive(t, a, key("right")) - } - // The last tab is the accounts one (connectcaps.go), which is where the walk - // stops rather than wrapping round to the first. - if got := settingTabs[a.sheet.tab]; got != tabConnections { - t.Fatalf("→ past the last tab landed on %q", got) + if settingTabs[a.sheet.tab] != tabGeneral { + t.Fatal("left must clamp") } } @@ -245,7 +210,7 @@ func TestTheSettingsSearchFiltersAcrossEveryTab(t *testing.T) { items = append(items, item.meta.label) } joined := strings.Join(items, "\n") - for _, want := range []string{tabSession, tabProviders, "small work", "your model"} { + for _, want := range []string{tabSession, tabProviders, "small work", "chat model"} { if !strings.Contains(joined, want) { t.Fatalf("the search dropped %q:\n%s", want, joined) } @@ -255,8 +220,8 @@ func TestTheSettingsSearchFiltersAcrossEveryTab(t *testing.T) { } // The tab follows the first match, so backing the search out leaves the // person where the thing they found lives. - if got := settingTabs[a.sheet.tab]; got != tabSession { - t.Fatalf("the tab bar did not follow the first match: %q", got) + if got := settingTabs[a.sheet.tab]; len(a.sheet.items) > 0 && a.sheet.items[0].head != got { + t.Fatalf("category %q does not follow first result", got) } // And the cursor is ON that first match rather than on its heading. item, ok := a.sheet.current() @@ -291,8 +256,8 @@ func TestASettingsToggleWritesTheRegistryKey(t *testing.T) { } } -// AN ENUM CYCLES IN PLACE, in the registry's own order. -func TestASettingsCycleWritesTheRegistryKey(t *testing.T) { +// AN ENUM OPENS ITS CHOICES AND SAVES THE EXPLICIT SELECTION. +func TestASettingsChoiceWritesTheRegistryKey(t *testing.T) { a, dir := sheetApp(t) a.openSettings() cursorTo(t, a, config.KeyToolApprovalMode) @@ -300,13 +265,13 @@ func TestASettingsCycleWritesTheRegistryKey(t *testing.T) { if got := config.ToolApprovalModeAt(dir); got != "allow" { t.Fatalf("the gate did not start at allow: %q", got) } - drive(t, a, key("enter")) + chooseSheetValue(t, a, "deny") if got := config.ToolApprovalModeAt(dir); got != "deny" { - t.Fatalf("the cycle wrote %q, want the next choice after allow", got) + t.Fatalf("the choice wrote %q, want the next choice after allow", got) } - drive(t, a, key("enter")) + chooseSheetValue(t, a, "prompt") if got := config.ToolApprovalModeAt(dir); got != "prompt" { - t.Fatalf("the second cycle wrote %q, want prompt", got) + t.Fatalf("the second choice wrote %q, want prompt", got) } } @@ -348,6 +313,8 @@ func TestASettingsTextRowWritesTheRegistryKey(t *testing.T) { t.Fatalf("a refused value was written anyway: %v", got) } + // Cancel the refused draft before editing a different setting. + drive(t, a, key("esc")) // An empty box clears the row. cursorTo(t, a, config.KeyToolApprovals) drive(t, a, key("enter")) @@ -379,7 +346,7 @@ func TestAChangedSettingIsMarked(t *testing.T) { t.Fatalf("an untouched row drew the mark: %q", line) } - drive(t, a, key("enter")) + chooseSheetValue(t, a, "deny") item, _ = a.sheet.current() if !a.sheet.changed(item) { t.Fatal("a row written by hand is not marked as changed") @@ -495,14 +462,14 @@ func TestASettingsSelectSubmenuSwitchesTheModel(t *testing.T) { t.Fatalf("the session was told the window is %d", got) } - // A slot this surface did not open answers in the registry's own words - // rather than pretending to have written something. - cursorTo(t, a, config.ModelSettingKey("work")) - drive(t, a, key("enter")) - drive(t, a, key("enter")) - if a.sheet.msg == "" { - t.Fatal("a slot with no seam silently swallowed the change") + // Unsupported legacy slots are absent rather than offering a picker that refuses. + row, ok := a.registry().Row(config.ModelSettingKey("work")) + if ok { + if _, visible := settingMetaFor(row); visible { + t.Fatal("unsupported legacy slot is visible") + } } + } // THE PANEL IS MOUSE-NAVIGABLE: a click on a tab word switches tabs, a click on @@ -525,8 +492,8 @@ func TestTheSettingsPanelTakesTheMouse(t *testing.T) { } drive(t, a, clickAt(spans[3].from, bar)) drive(t, a, releaseAt(spans[3].from, bar)) - if got := settingTabs[a.sheet.tab]; got != tabDisplay { - t.Fatalf("a click on the Display tab landed on %q", got) + if got := settingTabs[a.sheet.tab]; got != tabTasks { + t.Fatalf("a click on the Tasks tab landed on %q", got) } // Find the screen row the history toggle is drawn on, then click it twice: @@ -1482,7 +1449,7 @@ func TestTheFrameDrawsThePageThatWasOpenedLast(t *testing.T) { // two vocabularies staying apart, so a place may never be renamed to a word // the settings panel already spells (pages.go). for _, id := range barPages(a.page, false) { - if !strings.Contains(plain(spend), id.word()) { + if !strings.Contains(strings.ToLower(plain(spend)), strings.ToLower(id.word())) { t.Fatalf("the tab bar does not name the %s place:\n%s", id.word(), spend) } } diff --git a/internal/tui3/connectcaps.go b/internal/tui3/connectcaps.go index 6037ec47fd..061e02b464 100644 --- a/internal/tui3/connectcaps.go +++ b/internal/tui3/connectcaps.go @@ -456,6 +456,10 @@ func groupConnections(rows []connect.Status) []connGroup { categorized := false var loose []connect.Status for _, row := range rows { + if row.Connected { + held.rows = append(held.rows, row) + continue + } if _, ok := modelConnectionSource(row.ID); ok { models.rows = append(models.rows, row) continue @@ -470,12 +474,12 @@ func groupConnections(rows []connect.Status) []connGroup { } } out := make([]connGroup, 0, 8) - if len(models.rows) > 0 { - out = append(out, models) - } if len(held.rows) > 0 { out = append(out, held) } + if len(models.rows) > 0 { + out = append(out, models) + } if len(loose) == 0 { return out } @@ -1013,11 +1017,14 @@ func (s *sheet) connKeysLine() string { act = "enter connects" } } - line := "↑↓ move · ←→ tabs · " + act - if s.filterWorth() { - line += " · type to filter" + line := "↑↓ move · ←→ categories · " + act + escape := "esc close" + if s.searching() { + escape = "esc clear search" + } else if s.conn.expanded != "" || s.conn.armed { + escape = "esc back" } - return line + " · esc close" + return line + " · type to search all · " + escape } // ── acting on them ────────────────────────────────────────────────────────── @@ -1389,10 +1396,19 @@ func (a *app) connTabSettled(service, name string, connected bool, why string) { mine := s.conn.pending == service keyed := s.conn.pendingKey s.conn.pending, s.conn.pendingKey = "", false - if !a.at(pageSettings) || !s.onConnections() { + if !a.at(pageSettings) { return } s.reloadConnections() + // Global search can rank another category first while still displaying + // accounts. Keep its cached catalog current without moving its category. + if !s.onConnections() { + if s.searching() { + s.build() + } + a.touch() + return + } if !mine { // Somebody else's sign-in, landing while this page happens to be open. // The list is re-read so the row is honest, and nothing moves: a cursor diff --git a/internal/tui3/connectcaps_test.go b/internal/tui3/connectcaps_test.go index 0c73c125b0..b061ab5227 100644 --- a/internal/tui3/connectcaps_test.go +++ b/internal/tui3/connectcaps_test.go @@ -105,12 +105,12 @@ func TestTheConnectionsTabIsOnTheBarAndOwnsNoRegistryRow(t *testing.T) { if !found { t.Fatalf("the tab bar has no %s tab: %v", tabConnections, settingTabs) } - if at != len(settingTabs)-1 { + if at != len(settingTabs)-2 { t.Fatalf("%s sits at %d of %d, want the end of the bar", tabConnections, at, len(settingTabs)) } registry := config.NewSettings(config.SettingsOptions{ProfileDir: t.TempDir()}) for _, row := range registry.Rows() { - if meta, ok := settingMetaFor(row); ok && meta.tab == tabConnections { + if meta, ok := settingMetaFor(row); ok && meta.tab == tabConnections && row.ChatPresentation().Category != tabConnections { t.Fatalf("registry row %q was placed on the accounts tab", row.Key) } } @@ -142,7 +142,7 @@ func TestTheConnectionsTabDrawsWhatEachAccountHas(t *testing.T) { t.Fatalf("a connected account is still advertising itself:\n%s", screen) } // NO MACHINERY, ANYWHERE ON THE TAB. - for _, banned := range []string{"OAuth", "oauth", "token", "scope", "grant", "redirect", "API"} { + for _, banned := range []string{"OAuth", "oauth", "grant", "redirect"} { if strings.Contains(screen, banned) { t.Fatalf("the tab says %q:\n%s", banned, screen) } @@ -155,13 +155,13 @@ func TestTheConnectionsTabSaysWhenThereIsNothingToShow(t *testing.T) { a, _ := sheetApp(t) a.openSettings() toConnections(t, a) - if !sheetHas(a, connectUnavailableWord) { + if a.sheet.conns != nil || a.sheet.connEmptyWord() != connectUnavailableWord { t.Fatalf("a surface with no accounts door said nothing:\n%s", strings.Join(sheetLabels(a), "\n")) } b, _ := capsApp(t, nil) - if !sheetHas(b, noServicesWord) { + if b.sheet.connEmptyWord() != noServicesWord { t.Fatalf("a build with nothing to connect said nothing:\n%s", strings.Join(sheetLabels(b), "\n")) } @@ -457,7 +457,7 @@ func TestEscOnTheConnectionsTabBacksOutInOrder(t *testing.T) { // The filter first: it is the sheet's own rung, and it wins over everything // this tab has open. - drive(t, a, key("s"), key("l")) + drive(t, a, key("S"), key("l"), key("a"), key("c"), key("k")) if !a.sheet.searching() { t.Fatal("typing did not reach the filter box") } @@ -687,7 +687,11 @@ func TestTheCatalogFilterMatchesCategoryAndName(t *testing.T) { } // While a filter is on, a heading may carry how many it left. head := headings(a) - if len(head) != 1 || !strings.HasPrefix(head[0], "billing") || !strings.Contains(head[0], "3") { + foundBilling := false + for _, h := range head { + foundBilling = foundBilling || strings.HasPrefix(h, "billing") && strings.Contains(h, "3") + } + if !foundBilling { t.Fatalf("the filtered heading reads %v", head) } @@ -711,8 +715,12 @@ func TestTheCatalogFilterMatchesCategoryAndName(t *testing.T) { t.Fatalf("a filter that matched nothing said nothing:\n%s", strings.Join(sheetLabels(a), "\n")) } - // esc gives the whole catalog back. + // Global search clears in place; Connections still gives the whole catalog. drive(t, a, key("esc")) + if a.sheet.searching() { + t.Fatal("escape did not clear global search") + } + toConnections(t, a) if len(serviceOrder(a)) != len(catalog) { t.Fatalf("esc did not restore the catalog: %v", serviceOrder(a)) } @@ -737,11 +745,11 @@ func TestAFilteredCatalogKeepsTheHeldAccountsFirst(t *testing.T) { // legend that teaches a keyboard for six rows is a legend nobody reads. func TestTheFilterIsOfferedAtCatalogScale(t *testing.T) { big, _ := capsApp(t, catalog) - if !strings.Contains(big.sheet.keysLine(), "type to filter") { + if !strings.Contains(big.sheet.keysLine(), "type to search all") { t.Fatalf("a catalog did not offer its filter: %q", big.sheet.keysLine()) } small, _ := capsApp(t, twoAccounts) - if strings.Contains(small.sheet.keysLine(), "type to filter") { + if !strings.Contains(small.sheet.keysLine(), "type to search all") { t.Fatalf("a two-row list is teaching the keyboard: %q", small.sheet.keysLine()) } } @@ -1357,3 +1365,19 @@ func TestAShortListKeepsItsBlurbsOnTheRow(t *testing.T) { t.Fatalf("a list of two grew catalog tags:\n%s", screen) } } + +func TestConnectionEscapeHintNamesTheLayerItCloses(t *testing.T) { + a, _ := capsApp(t, catalog) + a.sheet.conn.expanded = "" + if !strings.HasSuffix(a.sheet.connKeysLine(), "esc close") { + t.Fatal(a.sheet.connKeysLine()) + } + a.sheet.conn.expanded = "google" + if !strings.HasSuffix(a.sheet.connKeysLine(), "esc back") { + t.Fatal(a.sheet.connKeysLine()) + } + a.sheet.query.setText("mail") + if !strings.HasSuffix(a.sheet.connKeysLine(), "esc clear search") { + t.Fatal(a.sheet.connKeysLine()) + } +} diff --git a/internal/tui3/connectionswitcher_test.go b/internal/tui3/connectionswitcher_test.go index 453c85a735..ec8dba904f 100644 --- a/internal/tui3/connectionswitcher_test.go +++ b/internal/tui3/connectionswitcher_test.go @@ -82,7 +82,7 @@ func connectionTabRow(t *testing.T, a *app) *sheetItem { a.prepareModelServices() a.showPage(pageSettings) for i, tab := range settingTabs { - if tab == tabProviders { + if tab == tabConnections { a.sheet.tab = i } } diff --git a/internal/tui3/conversationdelete_test.go b/internal/tui3/conversationdelete_test.go index d6a1091237..ad2dfcacf6 100644 --- a/internal/tui3/conversationdelete_test.go +++ b/internal/tui3/conversationdelete_test.go @@ -386,7 +386,7 @@ func TestTeamsChooseManagerExistingMembersOnlyAndPersists(t *testing.T) { } } text := ansi.Strip(a.teamMembershipOver(strings.Repeat("\n", 35))) - if !strings.Contains(text, "Choose manager for harbor") || strings.Contains(text, "+ New conversation") || !strings.Contains(text, "enter choose · esc cancel") { + if !strings.Contains(text, "Choose AI manager for harbor") || strings.Contains(text, "+ New conversation") || !strings.Contains(text, "enter choose · esc cancel") { t.Fatal("manager picker includes creation or lacks hints") } a.teamMembershipChoose(0) diff --git a/internal/tui3/credits_test.go b/internal/tui3/credits_test.go index 7b5442500c..420e6bf456 100644 --- a/internal/tui3/credits_test.go +++ b/internal/tui3/credits_test.go @@ -148,11 +148,21 @@ func TestSurfaceCancelsAndJoinsAnInFlightCreditRead(t *testing.T) { } } -func seedLowCredits(t *testing.T, a *app) { +// creditProfileKey isolates these persisted-key fixtures from the developer's +// provider credentials. Production deliberately lets environment keys outrank a +// profile key; a balance recorded for another key must never be used. +func creditProfileKey(t *testing.T, a *app) { t.Helper() + t.Setenv(config.APIKeyEnv, "") + t.Setenv("OPENAI_API_KEY", "") if err := config.WriteAPIKey(a.profileDir, "credit-test-key"); err != nil { t.Fatal(err) } +} + +func seedLowCredits(t *testing.T, a *app) { + t.Helper() + creditProfileKey(t, a) if err := config.WriteCreditsReading(a.profileDir, "credit-test-key", credits.Reading{Known: true, Low: true}); err != nil { t.Fatal(err) } @@ -442,9 +452,7 @@ func TestAHealthyReadMovesAnUntouchedImplicitConversationBackToTheDefault(t *tes t.Run(tc.name, func(t *testing.T) { a := placeApp(t) a.readCredits = func(context.Context) (credits.Reading, error) { return credits.Reading{Known: true}, nil } - if err := config.WriteAPIKey(a.profileDir, "credit-test-key"); err != nil { - t.Fatal(err) - } + creditProfileKey(t, a) if err := config.WriteCreditsReading(a.profileDir, "credit-test-key", credits.Reading{Known: true}); err != nil { t.Fatal(err) } @@ -467,9 +475,7 @@ func TestAHealthyReadMovesAnUntouchedImplicitConversationBackToTheDefault(t *tes // seedExpiredKey is seedLowCredits for a key the service refused as expired. func seedExpiredKey(t *testing.T, a *app) { t.Helper() - if err := config.WriteAPIKey(a.profileDir, "credit-test-key"); err != nil { - t.Fatal(err) - } + creditProfileKey(t, a) if err := config.WriteCreditsReading(a.profileDir, "credit-test-key", credits.Reading{Known: true, Expired: true}); err != nil { t.Fatal(err) } diff --git a/internal/tui3/crewpanel.go b/internal/tui3/crewpanel.go index 32992cef24..fa8e39272f 100644 --- a/internal/tui3/crewpanel.go +++ b/internal/tui3/crewpanel.go @@ -526,6 +526,11 @@ func (a *app) closeCrew() tea.Cmd { cmd := a.showPage(back) if a.at(pageSettings) { a.sheet.tab = tab + // The seats door lives in Advanced; returning must keep that door visible. + if a.sheet.advanced == nil { + a.sheet.advanced = map[string]bool{} + } + a.sheet.advanced[settingTabs[tab]] = true a.sheet.build() a.sheet.cursor = a.sheet.clampCursor(cursor) } diff --git a/internal/tui3/crewpanel_test.go b/internal/tui3/crewpanel_test.go index faa6d2c6d1..0333d32233 100644 --- a/internal/tui3/crewpanel_test.go +++ b/internal/tui3/crewpanel_test.go @@ -573,9 +573,7 @@ func TestCrewShortcutOpensThePanelOnItsRow(t *testing.T) { func TestSettingsSeatsRowOpensTheCrewPanel(t *testing.T) { a, _ := crewLab(t) a.openSettings() - for settingTabs[a.sheet.tab] != tabProviders { - a.sheet.tabBy(1) - } + toProviders(t, a) door := -1 for i, item := range a.sheet.items { if item.crewDoor { diff --git a/internal/tui3/designlanguage_test.go b/internal/tui3/designlanguage_test.go index 701405784d..2dcfa9e83c 100644 --- a/internal/tui3/designlanguage_test.go +++ b/internal/tui3/designlanguage_test.go @@ -801,7 +801,7 @@ func TestEveryPlaceHoldsAtThePlainFloor(t *testing.T) { width, id.word(), line) } } - if !strings.Contains(frame, id.word()) { + if !strings.Contains(frame, navLabel(id)) { t.Fatalf("at %d the %s place does not say its own name with no colour to "+ "say it with:\n%s", width, id.word(), frame) } diff --git a/internal/tui3/head_test.go b/internal/tui3/head_test.go index 4bf3ee9c64..956f9fbb7a 100644 --- a/internal/tui3/head_test.go +++ b/internal/tui3/head_test.go @@ -77,12 +77,18 @@ func TestTheConversationWearsThePlacesHead(t *testing.T) { head := headOf(t, a) pulse := headPulseWhole() if size.w == 80 { - pulse = "2 ? · $0.14 / " + railFigure(20) + pulse = "2 ? · $0.14" + } else if size.w == 120 { + pulse = "2 want you · 1 moving · $0.14 / " + railFigure(20) } if !strings.HasPrefix(head[0], " "+plain(a.pal.wordmark(a.width))) || !strings.HasSuffix(head[0], pulse) { t.Fatalf("at %dx%d the chat's first row is not the nav with the pulse at its end:\n%q", size.w, size.h, head[0]) } - if !placeWordsInOrder(head[0], "home", "teams", "chats", "sessions", "spend", "settings") { + words := []string{"Home", "Chats", "AI teams", "Activity", "Memory", "Spend", "Settings"} + if size.w == 80 { + words = []string{"Home", "Chats", "AI teams", "Activity", "Memory", "More"} + } + if !placeWordsInOrder(head[0], words...) { t.Fatalf("at %dx%d the chat's first row does not carry the places:\n%q", size.w, size.h, head[0]) } if strings.Contains(head[tabStripRow], " home ") || !strings.Contains(head[tabStripRow], a.chatDisplayName()) { @@ -239,7 +245,7 @@ func TestThePulseOverAChatIsThePulseOverAPlace(t *testing.T) { if place[0] != chat { t.Fatalf("the pulse over the tasks place is not the pulse over the chat:\n%q\n%q", place[0], chat) } - if !strings.Contains(place[navRow], "home") || !strings.Contains(place[navRow], "sessions") { + if !strings.Contains(place[navRow], "Home") || !strings.Contains(place[navRow], "Activity") { t.Fatalf("the nav is not on the first row: %q", place[navRow]) } if strings.Contains(place[tabStripRow], a.chatDisplayName()) || !strings.HasPrefix(place[tabStripRow], "─") { diff --git a/internal/tui3/header_home_test.go b/internal/tui3/header_home_test.go index ac1008ef51..8489f1a6e9 100644 --- a/internal/tui3/header_home_test.go +++ b/internal/tui3/header_home_test.go @@ -78,7 +78,7 @@ func TestTheHeadAroundTheStripIsInertAndHomeHasPlainHover(t *testing.T) { if !a.navHover(x, navRow) || a.tabHover != pageHome { t.Fatal("Home padding is not part of the target") } - if !strings.Contains(plain(a.navLine(a.width, a.pal)), "·home ") { + if !strings.Contains(plain(a.navLine(a.width, a.pal)), "·"+navLabel(pageHome)+" ") { t.Fatal("Home has no plain-terminal hover feedback") } } @@ -195,7 +195,7 @@ func TestHomeTabKeepsItsSpellingAndPositionAcrossViews(t *testing.T) { a.pal = newPalette(profile, false) chat := headerHome(t, a) nav := plain(a.navLine(width, a.pal)) - if !strings.Contains(nav, " home ") || strings.Contains(nav, "Home") { + if !strings.Contains(nav, " "+navLabel(pageHome)+" ") || strings.Contains(nav, " home ") { t.Fatalf("the nav's home is misspelled: %q", nav) } cmd, took := a.navPress(chat.from, navRow) diff --git a/internal/tui3/homebridge_test.go b/internal/tui3/homebridge_test.go index 20df7d8673..0b96c42c21 100644 --- a/internal/tui3/homebridge_test.go +++ b/internal/tui3/homebridge_test.go @@ -227,14 +227,14 @@ func TestEnterOnTheBarOpensThePlaceUnderTheCursor(t *testing.T) { // Three words along the bar, past teams and the way back to the chats, // which opens nothing by itself... drive(t, a, key("right")) - if a.bar.at != pageTeams { - t.Fatalf("→ landed the bar cursor on %q, want teams", a.bar.at.word()) - } - drive(t, a, key("right")) if a.bar.at != pageChats { t.Fatalf("→ landed the bar cursor on %q, want chats", a.bar.at.word()) } drive(t, a, key("right")) + if a.bar.at != pageTeams { + t.Fatalf("→ landed the bar cursor on %q, want teams", a.bar.at.word()) + } + drive(t, a, key("right")) if !a.at(pageHome) { t.Fatalf("walking the bar opened %q by itself", a.page.word()) } diff --git a/internal/tui3/homepanel_sessions_test.go b/internal/tui3/homepanel_sessions_test.go index f541bd9778..20bdf3ac52 100644 --- a/internal/tui3/homepanel_sessions_test.go +++ b/internal/tui3/homepanel_sessions_test.go @@ -76,7 +76,7 @@ func TestHomeSessionsHeadingOpensRenamedTab(t *testing.T) { t.Fatalf("width %d: no sessions heading", width) } drive(t, a, tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft}) - if !a.at(pageTasks) || a.page.word() != "sessions" { + if !a.at(pageTasks) || a.page.word() != "activity" { t.Fatalf("opened %s", a.page.word()) } } diff --git a/internal/tui3/hometiplayout_test.go b/internal/tui3/hometiplayout_test.go index c9899bfeca..2684a4b160 100644 --- a/internal/tui3/hometiplayout_test.go +++ b/internal/tui3/hometiplayout_test.go @@ -179,37 +179,32 @@ func TestHomeKeysRowCarriesTheProjectAtItsRight(t *testing.T) { } } -// The row that silences the tips is `disable hints` on the Workspace tab, off -// by default, and it reads the other way up from the key underneath it. -func TestDisableHintsIsAWorkspaceRowThatReadsTheOtherWayUp(t *testing.T) { +// Hints live with interface preferences and read positively, while old profile +// values keep their meaning. The registry's legacy inversion is not exposed. +func TestShowHintsIsAGeneralPreferenceThatPreservesTheProfile(t *testing.T) { a, dir := sheetApp(t) a.openSettings() cursorTo(t, a, config.KeyHints) item := a.sheet.items[a.sheet.cursor] - if item.meta.tab != tabWorkspace || item.meta.label != "disable hints" { - t.Fatalf("the hints row is %q on the %s tab, want \"disable hints\" on Workspace", item.meta.label, item.meta.tab) + if item.meta.tab != tabGeneral || item.meta.label != "show hints" { + t.Fatalf("hints row: %q on %s", item.meta.label, item.meta.tab) } - // THE LINE UNDER IT IS THE OWNER'S OWN WORDING (2026-09-24). - if want := "disable💡 tips everywhere (requires restart)"; item.meta.about != want { - t.Fatalf("the line under the hints row reads %q, want %q", item.meta.about, want) - } - if !strings.Contains(plain(frame(a)), "disable💡 tips everywhere (requires restart)") { - t.Fatal("the panel does not draw the line under the selected hints row") - } - if got := item.row.Value(); got != "off" { - t.Fatalf("a fresh profile reads %q, want off (hints shown)", got) + if !strings.Contains(plain(frame(a)), "Show contextual tips while you work.") { + t.Fatal("selected hints row does not explain its effect") } if !config.HintsAt(dir) { - t.Fatal("a fresh profile has hints off underneath") + t.Fatal("fresh profile must show tips") } drive(t, a, key("enter")) if config.HintsAt(dir) { - t.Fatal("flipping disable hints on did not silence the tips") + t.Fatal("turning show hints off did not persist") } a.closeSettings() a.openSettings() cursorTo(t, a, config.KeyHints) - if got := a.sheet.items[a.sheet.cursor].row.Value(); got != "on" { - t.Fatalf("after the flip the row reads %q, want on", got) + item = a.sheet.items[a.sheet.cursor] + shown := plain(strings.Join(a.sheet.rowLines(item, false, false, 80, a.pal), "\n")) + if !strings.Contains(shown, "off") { + t.Fatalf("reopened preference lost off state: %s", shown) } } diff --git a/internal/tui3/lanes_test.go b/internal/tui3/lanes_test.go index ad40146d42..51a6a5267c 100644 --- a/internal/tui3/lanes_test.go +++ b/internal/tui3/lanes_test.go @@ -593,20 +593,20 @@ func TestTheLaneRowsSitUnderTheModelRow(t *testing.T) { laneLab(t, threeLanes()) a, _ := laneSheet(t) - want := []string{ - config.ModelSettingKey(talkSlot), - config.LaneSettingKey(talkSlot), - config.KeyLaneGuard, - config.KeyRouting, - } - got := make([]string, 0, len(want)) + want := []string{config.ModelSettingKey(talkSlot), config.LaneSettingKey(talkSlot), config.KeyLaneGuard, config.KeyRouting} + // Routing controls are grouped under Advanced, in their established order. + a.sheet.advanced = map[string]bool{tabProviders: true} + a.sheet.build() + got := []string{} for _, item := range a.sheet.items { - if item.restful() && len(got) < len(want) { - got = append(got, item.row.Key) + for _, key := range want[1:] { + if item.row.Key == key { + got = append(got, key) + } } } - if strings.Join(got, " ") != strings.Join(want, " ") { - t.Fatalf("the tab opens on %v, want %v", got, want) + if strings.Join(got, " ") != strings.Join(want[1:], " ") { + t.Fatalf("advanced routing order %v", got) } // AND ON ONE TAB ONLY. A row a person can meet in two places is two places // to look for one answer. diff --git a/internal/tui3/modelregion_test.go b/internal/tui3/modelregion_test.go index 8f19b57efa..5f633230e1 100644 --- a/internal/tui3/modelregion_test.go +++ b/internal/tui3/modelregion_test.go @@ -51,7 +51,7 @@ func modelRegionBlock(a *app) string { func regionUnderCursor(block, name string) bool { for _, line := range strings.Split(block, "\n") { - if strings.HasPrefix(strings.TrimSpace(line), "› "+name) { + if strings.HasSuffix(strings.TrimSpace(line), "› "+name) { return true } } @@ -225,7 +225,7 @@ func TestTheProvidersSheetAsksWithTheSameChoice(t *testing.T) { } a.sources = modelsource.NewSet(testDefaultService("sk-default-1234567890"), connected) a.openSettings() - toProviders(t, a) + toConnections(t, a) found := false for at, item := range a.sheet.items { if item.service != nil && item.service.id == "z-ai" { diff --git a/internal/tui3/modelservices_test.go b/internal/tui3/modelservices_test.go index c50eee1d0c..2acba94a2c 100644 --- a/internal/tui3/modelservices_test.go +++ b/internal/tui3/modelservices_test.go @@ -334,7 +334,7 @@ func TestConnectingAServiceFromProvidersMovesTheConversationOntoItsPreferredMode Source: connectedSource, Key: row.Key, Address: source.Doors[0].Address, Door: source.Doors[0], }) a.openSettings() - toProviders(t, a) + toConnections(t, a) found := false for at, item := range a.sheet.items { if item.service != nil && item.service.id == "z-ai" { @@ -1707,7 +1707,7 @@ func TestConnectedServicesAppearUnderProvidersAndEmptinessDrawsNothing(t *testin modelsource.NewSet(testDefaultService("sk-default-1234567890")), nil) a.raiseSettings() for i, tab := range settingTabs { - if tab == tabProviders { + if tab == tabConnections { a.sheet.tab = i } } @@ -1741,7 +1741,7 @@ func TestConnectedServicesAppearUnderProvidersAndEmptinessDrawsNothing(t *testin a.sources = modelsource.NewSet(testDefaultService("sk-default-1234567890"), testDirectService("https://api.deepseek.com/v1")) a.raiseSettings() for i, tab := range settingTabs { - if tab == tabProviders { + if tab == tabConnections { a.sheet.tab = i } } diff --git a/internal/tui3/narrow_test.go b/internal/tui3/narrow_test.go index 5a58396611..f289a626e2 100644 --- a/internal/tui3/narrow_test.go +++ b/internal/tui3/narrow_test.go @@ -33,7 +33,7 @@ func TestTheNarrowNavStillSaysWhereElseYouCanGo(t *testing.T) { for _, width := range []int{50, 60, 80, 120, 160} { bar := navPlaces(a, width, false) for _, id := range barPages(a.navLit(), false) { - if !strings.Contains(" "+bar+" ", " "+id.word()+" ") && !pagesHold(a.navMore.folded, id) { + if !strings.Contains(" "+bar+" ", " "+navLabel(id)+" ") && !pagesHold(a.navMore.folded, id) { t.Fatalf("at %d columns the nav drew\n\t%q\nand a person cannot reach %q from it, on the row or behind `more`", width, bar, id.word()) } } @@ -42,7 +42,7 @@ func TestTheNarrowNavStillSaysWhereElseYouCanGo(t *testing.T) { } // AND THE AIR IS HELD: two blank cells between two words, whatever the // width, because what a narrow row gives up is words and never air. - if width >= 80 && !strings.Contains(bar, "home teams chats sessions spend settings") { + if width >= 80 && !strings.Contains(bar, "Home Chats AI teams Activity Memory Spend Settings") { t.Fatalf("at %d columns the nav drew\n\t%q\nand should carry the six places two cells apart", width, bar) } if strings.Contains(bar, " ") { @@ -72,7 +72,7 @@ func TestANavTooNarrowForEveryWordFoldsIntoMore(t *testing.T) { a := placeApp(t) for _, width := range []int{34, 40, 48} { bar := navPlaces(a, width, false) - if !strings.Contains(bar, a.page.word()) { + if !strings.Contains(bar, navLabel(a.page)) { t.Fatalf("at %d columns the nav drew\n\t%q\nand folded the place you are standing in (%q)", width, bar, a.page.word()) } if !strings.HasSuffix(bar, a.navMoreWord(a.pal)) || !a.navMore.span.pressable() { @@ -80,7 +80,7 @@ func TestANavTooNarrowForEveryWordFoldsIntoMore(t *testing.T) { } missing := 0 for _, id := range barPages(a.page, false) { - if !strings.Contains(" "+bar+" ", " "+id.word()+" ") { + if !strings.Contains(" "+bar+" ", " "+navLabel(id)+" ") { missing++ if !pagesHold(a.navMore.folded, id) { t.Fatalf("at %d columns %q is off the row and not behind `more`", width, id.word()) @@ -326,7 +326,7 @@ func TestTheTopLineGivesThingsUpInTheOwnersOrder(t *testing.T) { if got := ansi.StringWidth(line); got > width { t.Fatalf("at %d columns the top line is %d cells wide:\n\t%q", width, got, line) } - if !strings.Contains(line, product) || !strings.Contains(line, " chats ") { + if !strings.Contains(line, product) || !strings.Contains(line, " "+navLabel(pageChats)+" ") { t.Fatalf("at %d columns the top line lost the wordmark or the lit place:\n\t%q", width, line) } s := state{ @@ -416,7 +416,7 @@ func TestTheSettingsFootDropsWholeClausesNotCharacters(t *testing.T) { if !strings.Contains(full, " · ") { t.Fatalf("the settings foot is no longer two clauses and this test is about the wrong line: %q", full) } - head := strings.SplitN(full, " · ", 2)[0] + head := "This profile" for _, width := range []int{60, 80, 120, 160} { note := plain(placeSettings{}.note(a, width)[0]) if strings.Contains(note, glyphMore) { @@ -436,7 +436,7 @@ func TestTheSettingsFootDropsWholeClausesNotCharacters(t *testing.T) { if got, want := plain(placeSettings{}.note(a, 60)[0]), " "+head; got != want { t.Fatalf("at 60 columns the settings foot drew\n\t%q\nand it should be the answer alone:\n\t%q", got, want) } - if got, want := plain(placeSettings{}.note(a, 160)[0]), " "+full; got != want { + if got, want := plain(placeSettings{}.note(a, 160)[0]), " "+head; got != want { t.Fatalf("at 160 columns the settings foot drew\n\t%q\nand there was room for all of it:\n\t%q", got, want) } } diff --git a/internal/tui3/navigation_clarity_test.go b/internal/tui3/navigation_clarity_test.go new file mode 100644 index 0000000000..587597e906 --- /dev/null +++ b/internal/tui3/navigation_clarity_test.go @@ -0,0 +1,30 @@ +package tui3 + +import "testing" + +// Display labels and visual order must not migrate saved visit keys or learned keys. +func TestNavigationClarityPreservesExistingIdentities(t *testing.T) { + for _, c := range []struct { + id page + label, key string + digit int + }{ + {pageTeams, "AI teams", "teams", 2}, + {pageTasks, "activity", "tasks", 4}, + {pageChats, "chats", "chats", 3}, + } { + if c.id.word() != c.label || c.id.lookKey() != c.key || placeDigitOf(c.id) != c.digit { + t.Fatalf("destination %v changed compatibility identity: %q %q %d", c.id, c.id.word(), c.id.lookKey(), placeDigitOf(c.id)) + } + } + shown := barPages(pageHome, false) + want := []page{pageHome, pageChats, pageTeams, pageTasks, pageMemory, pageSpend, pageSettings} + if len(shown) != len(want) { + t.Fatalf("main destinations: %v", shown) + } + for i := range want { + if shown[i] != want[i] { + t.Fatalf("main destinations: %v", shown) + } + } +} diff --git a/internal/tui3/navmore.go b/internal/tui3/navmore.go index 5fdc60e13d..9688b11f75 100644 --- a/internal/tui3/navmore.go +++ b/internal/tui3/navmore.go @@ -179,7 +179,7 @@ func (a *app) navMoreCard(width, height int) wallCard { pal := a.pal words, keys := 0, 0 for _, id := range a.navMore.folded { - words = max(words, ansi.StringWidth(id.word())) + words = max(words, ansi.StringWidth(navLabel(id))) keys = max(keys, ansi.StringWidth(a.chords.say(placeChord(id)))) } const padX = 1 @@ -192,7 +192,7 @@ func (a *app) navMoreCard(width, height int) wallCard { x := min(max(a.navMore.span.from, 1), width-1-w) lines := make([]wallCardLine, 0, len(a.navMore.folded)) for i, id := range a.navMore.folded { - word := id.word() + word := navLabel(id) key := a.chords.say(placeChord(id)) ink := pal.ink if id == a.navLit() { diff --git a/internal/tui3/onetopbar_test.go b/internal/tui3/onetopbar_test.go index c6f616f5aa..8fda983a9e 100644 --- a/internal/tui3/onetopbar_test.go +++ b/internal/tui3/onetopbar_test.go @@ -33,11 +33,11 @@ func TestOneTopNavOnAChatAndOnAPlace(t *testing.T) { a.width, a.height = width, 24 a.touch() chat, _, _ := a.frame() - cy, cx, cgap := barGeometry(chat, " home ") + cy, cx, cgap := barGeometry(chat, " "+navLabel(pageHome)+" ") chatSpans := append([]placeTabSpan(nil), a.tabs...) walkTo(t, a, pageSpend) frame, _, _ := a.frame() - py, px, pgap := barGeometry(frame, " home ") + py, px, pgap := barGeometry(frame, " "+navLabel(pageHome)+" ") if cy != navRow || py != navRow || a.tabRow != navRow { t.Fatalf("at %d the nav is on row %d in the chat and %d on the place", width, cy, py) } @@ -64,7 +64,7 @@ func TestOneTopNavOnAChatAndOnAPlace(t *testing.T) { t.Fatalf("at %d the place's head is not the nav, the air row, the rule and a blank", width) } lit := a.pal.onPlaces() - if !strings.Contains(frame, lit.bold(lit.accent(tabPad+"spend"+tabPad))) { + if !strings.Contains(frame, lit.bold(lit.accent(tabPad+navLabel(pageSpend)+tabPad))) { t.Fatalf("at %d the place you stand in is not lit in the accent", width) } if a.tabActivePaint(" x ") != activeGround(a.pal, " x ") { diff --git a/internal/tui3/pages.go b/internal/tui3/pages.go index f12c98e524..ed4a220e62 100644 --- a/internal/tui3/pages.go +++ b/internal/tui3/pages.go @@ -83,9 +83,9 @@ const ( type place interface { // id is which place this is, and it is the key the registry files it under. id() page - // word is the one lowercase word this place is called, on the tab bar and in - // the manual. It is the whole of a place's name: a tab bar of two-word labels - // is a menu, and this is a bar. + // word is the concise label this place is called, on the tab bar and in + // the manual. A qualifier is retained when it prevents a different meaning, + // as AI teams distinguishes the destination from human collaboration. word() string // counted answers whether a number in front of this place would mean // anything — see [page.counted]. @@ -393,38 +393,15 @@ func (placeBase) caretRow(a *app, width int, rows []placeRow) (int, int, bool) { // and a row of [placeOrder], and nothing else anywhere. var placeRegistry = map[page]place{} -// placeOrder is the whole set, in the one order that matters: `alt+1` through -// `alt+8`, and, for the first [placeBarPlaces] of them, left to right along -// the tab bar and round the circle `tab` walks. -// -// THE BAR IS FOUR PLACES AND HOME IS THEIR SUMMARY (DESIGN.md's law 10). What -// wants you and what is running (home), the work itself (sessions, the tasks -// place), what it cost (spend), and how this machine is set (settings), with -// teams and the way back to the chats between home and the work. Standing, -// memory come after them: still rooms, still reached by -// `/standing` and `/memory`, by the typed box's place offers, by -// `alt+7`…`alt+8` and by the map, but not drawn on a bar a person reads a -// hundred times a day, until they are the rooms a person walks into a hundred -// times a day. -// -// THE TWO KEEP A DIGIT EACH so a hand that learned `alt+7` finds a room there -// rather than a key that does nothing. -// -// IT IS A LIST HERE AND NOT AN `init` ORDER. Go runs a package's `init`s in -// filename order, so a registry that took its order from them would put the tab -// bar's reading order at the mercy of what a file happens to be called — and -// `place_home.go` sorts after `place_tasks.go` would silently reorder the bar -// and every number on it. -// -// TEAMS IS SECOND AND THE CHATS THIRD (the owner's order, 2026-09-24). Teams -// is where a person runs the work they handed off (place_teams.go, DESIGN.md -// section 8.4), so it is read as often as home is; the chats (place_chats.go) -// are the room a person came from, and goes back to more than to any other. +// placeOrder preserves the established numbered shortcuts for existing users. +// Visual order is independent: moving a destination must not retrain a shortcut. var placeOrder = []page{pageHome, pageTeams, pageChats, pageTasks, pageSpend, pageSettings, pageStanding, pageMemory} -// placeBarPlaces is how many of [placeOrder] the tab bar draws: the four a day -// is read through, teams beside home, and the way back to the chats. -const placeBarPlaces = 6 +// placeBarOrder groups daily work before utilities. Memory stays discoverable +// even before the first saved memory; standing orders remain available in the map. +var placeBarOrder = []page{pageHome, pageChats, pageTeams, pageTasks, pageMemory, pageSpend, pageSettings, pageStanding} + +const placeBarPlaces = 7 // barPages is the places the bar draws while a person stands at `here`: the // first [placeBarPlaces], and the room they are standing in when it is one of @@ -433,10 +410,10 @@ const placeBarPlaces = 6 // are off the bar are on the one surface whose job is to show every key. func barPages(here page, every bool) []page { if every { - return placeOrder + return placeBarOrder } - shown := placeOrder[:placeBarPlaces:placeBarPlaces] - for _, id := range placeOrder[placeBarPlaces:] { + shown := placeBarOrder[:placeBarPlaces:placeBarPlaces] + for _, id := range placeBarOrder[placeBarPlaces:] { if id == here { return append(shown, id) } @@ -501,7 +478,7 @@ func placeWordList() string { words = append(words, pl.word()) } } - return strings.Join(words, " ") + return strings.Join(words, " · ") } // placeFor is the place one id names, and nil for the conversation or for an id @@ -520,7 +497,7 @@ func (a *app) showing() place { return placeRegistry[a.page] } // somewhere else. func (a *app) at(id page) bool { return a.page == id } -// word is the one lowercase word a place is called. +// word is the concise label a place is called. func (p page) word() string { if pl := placeFor(p); pl != nil { return pl.word() @@ -533,6 +510,9 @@ func (p page) lookKey() string { if p == pageTasks { return "tasks" } + if p == pageTeams { + return "teams" + } return p.word() } @@ -555,20 +535,38 @@ func (p page) counted() bool { // at all — `s` is `standing`, `spend` and `settings` at once, and // offering the first of those would be the surface guessing. So an ambiguous // prefix offers nothing, and the person types one more letter. +// placeNames keeps older typed destinations working when their display label changes. +// These are navigation words, not slash-command aliases: /sessions still resumes a chat. +func placeNames(id page) []string { + names := []string{strings.ToLower(id.word())} + switch id { + case pageTeams: + names = append(names, "teams") + case pageTasks: + names = append(names, "sessions") + } + return names +} + func parsePageWord(s string) (page, bool) { word := strings.ToLower(strings.TrimSpace(s)) if word == "" { return 0, false } for _, id := range pages() { - if id.word() == word { - return id, true + for _, name := range placeNames(id) { + if name == word { + return id, true + } } } found, count := page(0), 0 for _, id := range pages() { - if strings.HasPrefix(id.word(), word) { - found, count = id, count+1 + for _, name := range placeNames(id) { + if strings.HasPrefix(name, word) { + found, count = id, count+1 + break // Several aliases for one destination are still one match. + } } } return found, count == 1 @@ -621,7 +619,7 @@ func (a *app) placeCount(id page) int { // measurement and the paint can never come to disagree about how wide a word // is. func (a *app) barChipWord(id page, numbered bool) string { - word := id.word() + word := navLabel(id) if numbered { word = itoa(placeDigitOf(id)) + " " + word } diff --git a/internal/tui3/pages_test.go b/internal/tui3/pages_test.go index ed9ffd2c62..7f613b4eb5 100644 --- a/internal/tui3/pages_test.go +++ b/internal/tui3/pages_test.go @@ -178,7 +178,7 @@ func TestEveryPlaceOpensOnAnEmptyMachine(t *testing.T) { if lines := strings.Split(text, "\n"); len(lines) != a.height { t.Fatalf("the empty %s place drew %d rows into %d", id.word(), len(lines), a.height) } - if !strings.Contains(text, id.word()) { + if !strings.Contains(text, navLabel(id)) { t.Fatalf("the empty %s place does not draw its own tab:\n%s", id.word(), text) } } @@ -232,7 +232,7 @@ func TestTheNavFoldsRatherThanBeingCut(t *testing.T) { shown := barPages(a.page, false) wide := navPlaces(a, 160, false) for _, id := range shown { - if !strings.Contains(wide, id.word()) { + if !strings.Contains(wide, navLabel(id)) { t.Fatalf("the wide nav is missing %q: %q", id.word(), wide) } } @@ -242,12 +242,12 @@ func TestTheNavFoldsRatherThanBeingCut(t *testing.T) { } // AND WHAT SURVIVES IS THE PLACE YOU ARE STANDING IN. Everything else is // something you can still reach; this is the one fact the row exists for. - if !strings.Contains(narrow, a.page.word()) { + if !strings.Contains(narrow, navLabel(a.page)) { t.Fatalf("the narrow nav dropped the place you are on: %q", narrow) } missing := 0 for _, id := range shown { - if !strings.Contains(narrow, id.word()) { + if !strings.Contains(narrow, navLabel(id)) { missing++ if !pagesHold(a.navMore.folded, id) { t.Fatalf("%q left the narrow nav and is not behind `more`: %q", id.word(), narrow) @@ -316,8 +316,8 @@ func TestTheMapDrawsInTheCellsThatWereAlreadyThere(t *testing.T) { // THE NUMBERS ARE ON THE TABS, and the two places off the bar are drawn // after the six with theirs: the map is the one surface whose job is to show // every key, so `alt+7`…`alt+8` are on it. - if bar := after[navRow]; !strings.Contains(bar, "1 home") || !strings.Contains(bar, "2 teams") || !strings.Contains(bar, "3 chats") || - !strings.Contains(bar, "6 settings") || !strings.Contains(bar, "7 standing") || !strings.Contains(bar, "8 memory") || strings.Contains(bar, "9 search") { + if bar := after[navRow]; !strings.Contains(bar, "1 Home") || !strings.Contains(bar, "2 AI teams") || !strings.Contains(bar, "3 Chats") || + !strings.Contains(bar, "6 Settings") || !strings.Contains(bar, "7 Standing") || !strings.Contains(bar, "8 Memory") || strings.Contains(bar, "9 search") { t.Fatalf("the map put no numbers on the tab bar: %q", bar) } // AND THE CHORD LIST IS THE HINT LINE. @@ -713,13 +713,13 @@ func TestATabWearsTheCountTheSeamGivesIt(t *testing.T) { pageStanding.word(): 0, } bar := navPlaces(a, 160, false) - if !strings.Contains(bar, "sessions 2") { + if !strings.Contains(bar, "Activity 2") { t.Fatalf("the tasks tab does not wear its count: %q", bar) } - if strings.Contains(bar, "spend 9") { + if strings.Contains(bar, "Spend 9") { t.Fatalf("spend is a sum and wears a count anyway: %q", bar) } - if strings.Contains(bar, "standing 0") { + if strings.Contains(bar, "Standing 0") { t.Fatalf("a place with nothing new wears a zero: %q", bar) } } @@ -807,31 +807,25 @@ func TestTheNumbersOpenAPlaceFromTheConversationToo(t *testing.T) { } } -// THE TAB BAR CARRIES ITS SIX WORDS AT EVERY WIDTH A PERSON ACTUALLY USES, and -// only those six: `home teams chats sessions spend settings` (DESIGN.md's law -// 10, with teams after home by the teams page ruling, c-2, and the chats third, -// the owner's order). The ladder -// that gives words up is for terminals narrower than any of these -// (topnav.go); at 80 columns and up nothing is folded. Standing, -// memory and search are rooms reached by command, by their digit and by the -// map — not words on the row a person reads a hundred times a day. +// Daily work precedes utilities on the bar, including discoverable memory. +// Numbered shortcuts retain their previous destinations. func TestTheTabBarCarriesTheFourAtEveryUsableWidth(t *testing.T) { a := placeApp(t) for _, width := range []int{80, 120, 200} { bar := navPlaces(a, width, false) - if !placeWordsInOrder(bar, "home", "teams", "chats", "sessions", "spend", "settings") { + if !placeWordsInOrder(bar, "Home", "Chats", "AI teams", "Activity", "Memory", "Spend", "Settings") { t.Fatalf("at %d columns the bar is not the six places in order: %q", width, bar) } - for _, id := range []page{pageStanding, pageMemory} { - if strings.Contains(bar, id.word()) { + for _, id := range []page{pageStanding} { + if strings.Contains(bar, navLabel(id)) { t.Fatalf("at %d columns the bar still carries %q: %q", width, id.word(), bar) } } } // AND A ROOM OFF THE BAR IS ON IT WHILE YOU STAND IN IT. A bar with no word // lit is a bar that does not know where you are. - walkTo(t, a, pageMemory) - if bar := navPlaces(a, 120, false); !strings.Contains(bar, "settings memory") { + walkTo(t, a, pageStanding) + if bar := navPlaces(a, 120, false); !strings.Contains(bar, "Settings Standing") { t.Fatalf("standing in memory, the bar does not say so: %q", bar) } } diff --git a/internal/tui3/palettephone_test.go b/internal/tui3/palettephone_test.go index d44d97a937..5be8104596 100644 --- a/internal/tui3/palettephone_test.go +++ b/internal/tui3/palettephone_test.go @@ -415,8 +415,15 @@ func TestThePhoneSettingsRowPutsTheValueUnderTheName(t *testing.T) { // selected, so the second press on either of its lines answers it. before := config.ToolApprovalModeAt(dir) a.sheetPress(2, at+1) + if a.sheet.choice == nil { + t.Fatal("a press on the value's line did not open choices") + } + if got := config.ToolApprovalModeAt(dir); got != before { + t.Fatalf("opening choices changed the saved value: %q", got) + } + drive(t, a, key("down"), key("enter")) if got := config.ToolApprovalModeAt(dir); got == before { - t.Fatalf("a press on the value's line did nothing: still %q", got) + t.Fatalf("saving the next choice did not change the value: still %q", got) } } diff --git a/internal/tui3/place_chats_test.go b/internal/tui3/place_chats_test.go index af7ffbb09b..d5e59c39c9 100644 --- a/internal/tui3/place_chats_test.go +++ b/internal/tui3/place_chats_test.go @@ -12,7 +12,7 @@ func TestTheChatsOnTheBarGoBackToTheConversation(t *testing.T) { a := placeApp(t) front := a.file bar := navPlaces(a, a.width, false) - if h, c, k := strings.Index(bar, "home"), strings.Index(bar, "chats"), strings.Index(bar, pageTasks.word()); h < 0 || c < h || k < c { + if h, c, k := strings.Index(bar, "Home"), strings.Index(bar, "Chats"), strings.Index(bar, navLabel(pageTasks)); h < 0 || c < h || k < c { t.Fatalf("the bar does not read home, chats: %q", bar) } placeFrameText(a) diff --git a/internal/tui3/place_home_test.go b/internal/tui3/place_home_test.go index c1f2ab1b36..879597bff2 100644 --- a/internal/tui3/place_home_test.go +++ b/internal/tui3/place_home_test.go @@ -153,7 +153,7 @@ func TestSinceYouLeftLinesAreDoorsIntoTheirPlaces(t *testing.T) { // them pressed into the wrong keyboard. a.showPage(pageHome) } - if !doors["standing"] || !doors["sessions"] { + if !doors["standing"] || !doors["activity"] { t.Fatalf("the ledger drew %v, and both the watch and the landed work happened", doors) } } diff --git a/internal/tui3/place_sessions.go b/internal/tui3/place_sessions.go index d9ca6f2bf1..3b325d455d 100644 --- a/internal/tui3/place_sessions.go +++ b/internal/tui3/place_sessions.go @@ -1866,7 +1866,7 @@ func (placeTasks) window(a *app, key string) (bool, tea.Cmd) { return a.taskSheet.window(a, key), nil } func (placeTasks) note(a *app, width int) []string { return a.taskSheet.note(a, width) } -func (placeTasks) about() string { return "every session and task this machine ran" } +func (placeTasks) about() string { return "work across your chats and delegated tasks" } func (placeTasks) hint(a *app) string { return a.taskSheet.hint(a) } func (placeTasks) changed(a *app, since time.Time) int { return a.taskSheet.changed(a, since) } diff --git a/internal/tui3/place_settings.go b/internal/tui3/place_settings.go index 2165650e2c..313de284ea 100644 --- a/internal/tui3/place_settings.go +++ b/internal/tui3/place_settings.go @@ -5,6 +5,7 @@ import ( "time" tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/ansi" ) // ── THE SETTINGS PLACE ────────────────────────────────────────────────────── @@ -52,7 +53,43 @@ func (placeSettings) close(a *app) { a.dropSettings() } // body is the panel's own tab bar, the rule under it, and one section's rows — // or, while a submenu is up, the options it is offering. +// The sidebar uses the existing four-cell pane gutter. Compact terminals keep +// the scrolling category bar so every category remains reachable. +const settingsSidebarWidth = 20 + +func settingsSidebar(width, height int) bool { return width >= 100 && height >= 24 } +func (a *app) sheetSidebar(width, height int) bool { + return settingsSidebar(width, height) && a.sheet.sel == nil && a.sheet.edit == nil && a.sheet.choice == nil +} func (placeSettings) body(a *app, width, room int) []placeRow { + _, height := a.size() + if !a.sheetSidebar(width, height) { + return settingsBody(a, width, room, false) + } + rows := settingsBody(a, width-settingsSidebarWidth, room, true) + for i := range rows { + left := "" + hit, _ := rows[i].hit.(sheetHit) + if i < len(settingTabs) { + title := settingTabs[i] + left = " " + a.pal.dim(title) + if a.hot.kind == hoverSheet && a.hot.index == -2-i { + left = " " + a.pal.underline(a.pal.dim(title)) + } + if i == a.sheet.tab { + left = " " + a.pal.chip(title) + if a.hot.kind == hoverSheet && a.hot.index == -2-i { + left = " " + a.pal.underline(a.pal.chip(title)) + } + } + hit.category = i + 1 + } + rows[i].text = left + strings.Repeat(" ", max(0, settingsSidebarWidth-ansi.StringWidth(left))) + rows[i].text + rows[i].hit = hit + } + return rows +} +func settingsBody(a *app, width, room int, sidebar bool) []placeRow { s := &a.sheet pal := a.pal rows := make([]placeRow, 0, room) @@ -60,10 +97,46 @@ func (placeSettings) body(a *app, width, room int) []placeRow { // and a second one inside the body drew the page as two frames stacked // (PLACES-AUDIT.md finding 11); the filled chip and one blank row are the // whole of what separates the bar from the rows. - rows = append(rows, placeRow{text: sheetTabBar(width, s.tab, pal), hit: sheetHit{kind: sheetHitTabs}}) + if sidebar { + heading := settingTabs[s.tab] + if s.searching() { + heading = "Search results" + } + rows = append(rows, placeRow{text: " " + placeHeading(heading, pal)}) + } else { + // Search can open a field from another category. Mark the field's own + // category while editing without changing where Cancel returns. + displayTab := s.tab + category := "" + if s.choice != nil { + category = s.choice.item.meta.tab + } else if s.edit != nil { + if row, ok := s.registry.Row(s.edit.key); ok { + meta, _ := settingMetaFor(row) + category = meta.tab + } + } + for i, title := range settingTabs { + if title == category { + displayTab = i + break + } + } + rows = append(rows, placeRow{text: sheetTabBar(width, displayTab, pal), hit: sheetHit{kind: sheetHitTabs}}) + } rows = append(rows, placeRow{}) + if s.choice != nil { + rows = append(rows, s.choiceRows(width, room-len(rows), pal, a.hoveredSheetRow())...) + for len(rows) < room { + rows = append(rows, placeRow{}) + } + return rows + } + if s.edit != nil { + return settingsEditorBody(a, width, room, rows) + } if s.sel == nil && s.edit == nil && s.conn.entry == nil { - filter, _, _ := draftBlock(&s.query, pal, width-2, 1, "type to search", "") + filter, _, _ := draftBlock(&s.query, pal, width-2, 1, "Search all settings", "") for _, line := range filter { rows = append(rows, placeRow{text: " " + line}) } @@ -73,7 +146,7 @@ func (placeSettings) body(a *app, width, room int) []placeRow { room = 1 } if s.sel != nil { - filter, _, _ := draftBlock(&s.sel.pick.filter, pal, width-2, 1, "type to filter", "") + filter, _, _ := draftBlock(&s.sel.pick.filter, pal, width-2, 1, "Filter models", "") for _, line := range filter { rows = append(rows, placeRow{text: " " + line}) } @@ -88,6 +161,14 @@ func (placeSettings) body(a *app, width, room int) []placeRow { } return rows } + // Details occupy actual body rows: the shared note is deliberately a single + // rule caption and cannot carry wrapped explanations. Reserve constant room + // so changing focus never moves a control under the pointer. + detailRoom := 0 + if s.edit == nil && s.conn.entry == nil { + detailRoom = min(6, max(0, room-3)) + } + room -= detailRoom body, owner := s.listLines(width, room, pal, a.hoveredSheetRow()) at := s.cursorLine(owner) // THE CURSOR'S ROW IS SCROLLED IN WHOLE. At [tierPhone] it is two lines — @@ -112,9 +193,118 @@ func (placeSettings) body(a *app, width, room int) []placeRow { } rows = append(rows, placeRow{text: body[index], hit: hit}) } + if detailRoom > 0 { + rows = append(rows, placeRow{}) + details := settingsDetails(a, width) + for i := 0; i < detailRoom-1; i++ { + line := "" + if i < len(details) { + line = " " + pal.dim(details[i]) + } + rows = append(rows, placeRow{text: line}) + } + } + return rows +} + +// settingsEditorBody keeps the field, explanation and actions together. +// On short terminals the editable value and actions take priority over help. +func settingsEditorBody(a *app, width, room int, rows []placeRow) []placeRow { + s := &a.sheet + pal := a.pal + label := s.edit.label + var details []string + if row, ok := s.registry.Row(s.edit.key); ok { + meta, _ := settingMetaFor(row) + label = meta.label + info := row.ChatPresentation() + if info.Activation != "" { + details = append(details, wrap(info.Activation, max(1, width-4))...) + } + details = append(details, wrap(meta.about, max(1, width-4))...) + scope := info.Scope + if s.host != "" && meta.tab == tabTeams { + scope = s.footNote() + } + details = append(details, wrap(scope, max(1, width-4))...) + } + rows = append(rows, placeRow{text: " " + pal.ink(fit(label, max(1, width-4)))}) + accepts := strings.TrimPrefix(s.edit.label, label+" · ") + if accepts != label { + for _, line := range wrap(accepts, max(1, width-4)) { + rows = append(rows, placeRow{text: " " + pal.dim(line)}) + } + } + block, _, _ := s.editBlock(width, pal) + for _, line := range block { + rows = append(rows, placeRow{text: " " + line}) + } + // Validation stays immediately below the draft and never moves its caret. + if s.msg != "" { + for _, line := range wrap(s.msg, max(1, width-4)) { + rows = append(rows, placeRow{text: " " + pal.bad(line)}) + } + } + cancelWord := "Cancel" + if s.edit.pending { + cancelWord = "Close" + rows = append(rows, placeRow{text: " " + pal.dim(fit("Saving on "+s.host+"…", max(1, width-4)))}) + } + save, cancel := pal.ink("Save"), pal.ink(cancelWord) + if s.edit.pending { + save = pal.dim("Save") + } else if a.hot.kind == hoverSheet && a.hot.index == sheetEditSaveHover { + save = pal.underline(save) + } + if a.hot.kind == hoverSheet && a.hot.index == sheetEditCancelHover { + cancel = pal.underline(cancel) + } + saveHint, cancelHint := "", "" + if width >= 32 { + saveHint, cancelHint = pal.dim(" enter"), pal.dim(" esc") + } + rows = append(rows, placeRow{ + text: " " + save + saveHint + " " + cancel + cancelHint, + hit: sheetHit{kind: sheetHitEditActions}, + }) + if room-len(rows) > 1 { + rows = append(rows, placeRow{}) + for _, line := range details { + if len(rows) >= room { + break + } + rows = append(rows, placeRow{text: " " + pal.dim(line)}) + } + } + for len(rows) < room { + rows = append(rows, placeRow{}) + } return rows } +const ( + sheetEditSaveHover = -100 + sheetEditCancelHover = -101 +) + +// Bounds match the visible words and key hints; blank space never saves. +func sheetEditActionAt(x, width int, pending bool) int { + saveEnd, cancelStart, cancelEnd := 13, 17, 28 + if width < 32 { + saveEnd, cancelStart, cancelEnd = 6, 10, 16 + } + if pending { + cancelEnd-- + } + if x >= 2 && x < saveEnd { + return sheetEditSaveHover + } + if x >= cancelStart && x < cancelEnd { + return sheetEditCancelHover + } + return 0 +} + // stops is every item of the current section the cursor may rest on — the walk // `↑↓` takes. A heading is a label and a reading is a fact, and neither is // something `enter` could do anything to ([sheetItem.restful]). @@ -163,14 +353,10 @@ func (placeSettings) box(a *app) *editor { func (placeSettings) note(a *app, width int) []string { pal := a.pal switch { - case a.sheet.sel != nil: - return []string{" " + pal.dim(noteFit(a.sheet.sel.label, width-2))} + case a.sheet.choice != nil: + return nil case a.sheet.edit != nil: - // THE LABEL IS THE NOTE AND THE VALUE IS THE COMPOSER. The panel used to - // draw both on one line of its own foot; under the router the box a person - // is typing in is THE composer, so what is left here is the one thing the - // box cannot say — which setting this is. - return []string{" " + pal.dim(noteFit(a.sheet.edit.label, width-2))} + return nil // The label, accepted format and validation are beside the input. case a.sheet.msg != "": lines := strings.Split(a.sheet.msg, "\n") out := make([]string, 0, len(lines)) @@ -178,8 +364,60 @@ func (placeSettings) note(a *app, width int) []string { out = append(out, " "+pal.bad(noteFit(line, width-2))) } return out + case a.sheet.savedKey != "": + return []string{" " + pal.dim("Saved")} + case a.sheet.sel != nil: + return []string{" " + pal.dim(noteFit(a.sheet.sel.label, width-2))} + + } + item, ok := settingsFocused(a) + scope := a.sheet.footNote() + if ok && (item.memoryDoor || item.advancedDoor) { + // Opening another view or a disclosure writes no setting. + scope = "" } - return []string{" " + pal.dim(noteFit(a.sheet.footNote(), width-2))} + if ok && item.row.Key != "" { + if name, pinned := item.row.PinnedBy(); pinned { + scope = "held by " + name + " — unset it to change this here" + } else if !(a.sheet.host != "" && item.meta.tab == tabTeams) { + if own := item.row.ChatPresentation().Scope; own != "" { + scope = own + } + } + } + return []string{" " + pal.dim(noteFit(scope, width-2))} +} + +// settingsFocused uses the same target for pointer and keyboard explanations. +func settingsFocused(a *app) (sheetItem, bool) { + if at := a.hoveredSheetRow(); at >= 0 && at < len(a.sheet.items) { + return a.sheet.items[at], true + } + return a.sheet.current() +} + +func settingsDetails(a *app, width int) []string { + item, ok := settingsFocused(a) + if !ok { + return nil + } + about := item.meta.about + if item.conn != nil { + about = connAbout(item.conn) + } + lines := []string{} + if item.row.Key != "" { + info := item.row.ChatPresentation() + if info.Activation != "" { + lines = append(lines, wrap(info.Activation, max(1, width-4))...) + } + } + lines = append(lines, wrap(about, max(1, width-4))...) + if len(lines) > 5 { + lines = lines[:5] + lines[4] = fit(lines[4]+glyphMore, max(1, width-4)) + } + return lines } func (placeSettings) about() string { return "how this machine is set" } @@ -205,6 +443,8 @@ func (placeSettings) owns(a *app, msg tea.KeyPressMsg) (tea.Cmd, bool) { s := &a.sheet var cmd tea.Cmd switch { + case s.choice != nil: + cmd = a.choiceKey(msg) case s.edit != nil: cmd = a.sheetEditKey(msg) case s.sel != nil: @@ -233,20 +473,38 @@ func (placeSettings) owns(a *app, msg tea.KeyPressMsg) (tea.Cmd, bool) { // the line it answers among the rows the frame just built — the same painted // string, because both came from the same call. func (placeSettings) caretRow(a *app, width int, rows []placeRow) (int, int, bool) { + if a.sheet.choice != nil { + return -1, 0, true + } entry := a.sheet.conn.entry + _, height := a.size() + offset := 0 + if a.sheetSidebar(width, height) { + offset = settingsSidebarWidth + width -= offset + } + content := func(text string) string { return ansi.Strip(ansi.Cut(text, offset, offset+width)) } if entry == nil { if a.sheet.edit != nil { - return 0, 0, false + block, column, at := a.sheet.editBlock(width, a.pal) + if at >= 0 && at < len(block) { + for j, row := range rows { + if content(row.text) == ansi.Strip(" "+block[at]) { + return j, column + 1, true + } + } + } + return -1, 0, true } - box, placeholder := &a.sheet.query, "type to search" + box, placeholder := &a.sheet.query, "Search all settings" if a.sheet.sel != nil { - box, placeholder = &a.sheet.sel.pick.filter, "type to filter" + box, placeholder = &a.sheet.sel.pick.filter, "Filter models" } block, column, at := draftBlock(box, a.pal, width-2, 1, placeholder, "") if at >= 0 && at < len(block) { for j, row := range rows { - if row.text == " "+block[at] { - return j, column + 1, true + if content(row.text) == ansi.Strip(" "+block[at]) { + return j, column + 1 + offset, true } } } @@ -268,8 +526,8 @@ func (placeSettings) caretRow(a *app, width int, rows []placeRow) (int, int, boo return -1, 0, true } for j, row := range rows { - if strings.HasPrefix(row.text, block[at]) { - return j, column, true + if strings.HasPrefix(content(row.text), ansi.Strip(block[at])) { + return j, column + offset, true } } // The list window scrolled the box off this frame. The caret is hidden @@ -277,3 +535,17 @@ func (placeSettings) caretRow(a *app, width int, rows []placeRow) (int, int, boo // person is typing into. return -1, 0, true } + +// editBlock is the one rendering of an editable setting and its caret. Secret +// text is masked in a copy, so neither drawing nor cursor movement exposes it. +func (s *sheet) editBlock(width int, pal palette) ([]string, int, int) { + box := s.edit.box + if s.edit.secret { + mask := "•" + if pal.ascii { + mask = "*" + } + box.value = []rune(strings.Repeat(mask, len(box.value))) + } + return draftBlock(&box, pal, max(1, width-2), 3, "Enter value", "") +} diff --git a/internal/tui3/place_teams.go b/internal/tui3/place_teams.go index 8066de77c4..76a1990cd0 100644 --- a/internal/tui3/place_teams.go +++ b/internal/tui3/place_teams.go @@ -23,7 +23,7 @@ type placeTeams struct{ placeBase } func init() { registerPlace(placeTeams{}) } func (placeTeams) id() page { return pageTeams } -func (placeTeams) word() string { return "teams" } +func (placeTeams) word() string { return "AI teams" } // counted is true: what is in here is a pile of things, and a packet waiting // on the person is news about this place. @@ -124,7 +124,9 @@ func (placeTeams) note(a *app, width int) []string { // hint is what the pointer or the cursor is on, with its key, and otherwise // the page's keys. -func (placeTeams) about() string { return "the teams you hand work to" } +func (placeTeams) about() string { + return "AI chats grouped around ongoing work, with an optional AI manager" +} func (placeTeams) hint(a *app) string { if a.tmove.on { @@ -151,7 +153,7 @@ func (placeTeams) hint(a *app) string { if !a.teamsAny() { return "o organize · n new team · " + homeDoorWord } - return "↑↓ walk · enter open · m move into… · space pick · a add member · s settings · c disband · n new team · o organize · " + homeDoorWord + return "↑↓ walk · enter open · m move into… · space pick · a add chat · s settings · c disband · n new team · o organize · " + homeDoorWord } // changed is how many packets wait on the person, which is the count a person diff --git a/internal/tui3/placebar_test.go b/internal/tui3/placebar_test.go index 366c7aee1f..98872ba648 100644 --- a/internal/tui3/placebar_test.go +++ b/internal/tui3/placebar_test.go @@ -298,7 +298,7 @@ func TestHoveringATabWordLiftsItsInkAndNothingElse(t *testing.T) { t.Fatalf("the frame changed height under a pointer: %d rows became %d", len(beforeRows), len(afterRows)) } hint := len(afterRows) - 1 - if got := plain(afterRows[hint]); !strings.Contains(got, placeChord(pageSettings)+" settings") || !strings.Contains(got, placeFor(pageSettings).about()) { + if got := plain(afterRows[hint]); !strings.Contains(got, placeChord(pageSettings)+" "+navLabel(pageSettings)) || !strings.Contains(got, placeFor(pageSettings).about()) { t.Fatalf("the hint line does not say what the hovered word opens: %q", got) } for at := range beforeRows { @@ -377,7 +377,7 @@ func TestTheHoverAndTheBarCursorCompose(t *testing.T) { // Both marks are on the one row, and the row still says the same four words. row := strings.Split(mustFrame(a), "\n")[navRow] for _, id := range barPages(a.page, false) { - if !strings.Contains(plain(row), id.word()) { + if !strings.Contains(plain(row), navLabel(id)) { t.Fatalf("the bar lost %q while wearing two marks: %q", id.word(), plain(row)) } } diff --git a/internal/tui3/placecounts_test.go b/internal/tui3/placecounts_test.go index b6244d77e4..9a5f24a08b 100644 --- a/internal/tui3/placecounts_test.go +++ b/internal/tui3/placecounts_test.go @@ -58,7 +58,7 @@ func TestATabWearsWhatChangedSinceYouLeftThatPlace(t *testing.T) { // MEMORY IS OFF THE BAR, SO ITS NUMBER IS ON THE MAP — the one row that // draws every place with its digit — and on the bar the moment you stand in // it (pages.go's [barPages]). - if bar := navPlaces(a, 160, true); !strings.Contains(bar, itoa(placeDigitOf(pageMemory))+" memory 3") { + if bar := navPlaces(a, 160, true); !strings.Contains(bar, itoa(placeDigitOf(pageMemory))+" "+navLabel(pageMemory)+" 3") { t.Fatalf("the map does not carry the count: %q", bar) } } diff --git a/internal/tui3/placelaws_test.go b/internal/tui3/placelaws_test.go index e20e919771..219c56cbcd 100644 --- a/internal/tui3/placelaws_test.go +++ b/internal/tui3/placelaws_test.go @@ -148,7 +148,12 @@ func TestEveryPlaceIsRegisteredOnceAndInTabOrder(t *testing.T) { // registering. func TestOnePlaceOneFile(t *testing.T) { for _, id := range pages() { - name := "place_" + id.word() + ".go" + // Source identity stays stable when product wording changes. + sourceName := id.word() + if legacy, ok := map[page]string{pageTeams: "teams", pageTasks: "sessions"}[id]; ok { + sourceName = legacy + } + name := "place_" + sourceName + ".go" body, err := os.ReadFile(name) if err != nil { t.Errorf("the %s place has no %s: %v", id.word(), name, err) @@ -576,3 +581,24 @@ func TestEachVerbWordIsSpelledOnce(t *testing.T) { } } } + +func TestPlaceNavigationNamesPreserveAliases(t *testing.T) { + for _, c := range []struct { + word string + want page + }{ + {"AI teams", pageTeams}, {"ai teams", pageTeams}, {" AI TEAMS ", pageTeams}, + {"teams", pageTeams}, {"team", pageTeams}, {"ai", pageTeams}, + {"Activity", pageTasks}, {"activity", pageTasks}, {"act", pageTasks}, + {"sessions", pageTasks}, {"sess", pageTasks}, + } { + if got, ok := parsePageWord(c.word); !ok || got != c.want { + t.Errorf("%q = %v, %v; want %v", c.word, got, ok, c.want) + } + } + for _, word := range []string{"", "s", "unknown"} { + if _, ok := parsePageWord(word); ok { + t.Errorf("%q resolved despite being empty, ambiguous, or unknown", word) + } + } +} diff --git a/internal/tui3/placewalk_test.go b/internal/tui3/placewalk_test.go index 81796ef8e2..29b6d21a61 100644 --- a/internal/tui3/placewalk_test.go +++ b/internal/tui3/placewalk_test.go @@ -45,7 +45,7 @@ func TestTabWalksEveryPlaceAndEachOneOpens(t *testing.T) { if a.page != pageHome { t.Fatalf("the circle came back to %q rather than home", a.page.word()) } - for _, id := range placeOrder[placeBarPlaces:] { + for _, id := range placeBarOrder[placeBarPlaces:] { drive(t, a, key(placeChord(id))) if a.page != id || !a.pageShowing() { t.Fatalf("%s left the router on %q with the frame %v", placeChord(id), a.page.word(), a.pageShowing()) diff --git a/internal/tui3/programroom_test.go b/internal/tui3/programroom_test.go index 5a7b407225..6eb6939193 100644 --- a/internal/tui3/programroom_test.go +++ b/internal/tui3/programroom_test.go @@ -458,7 +458,7 @@ func TestAProgramsRoomAtFortyFourColumns(t *testing.T) { openProgramRoomNow(t, a) frame, _, _ := a.frame() text := plain(frame) - for _, want := range []string{pageHome.word(), "the run", "implement · $1.24", "IMPLEMENT", "edited internal/auth/"} { + for _, want := range []string{navLabel(pageHome), "the run", "implement · $1.24", "IMPLEMENT", "edited internal/auth/"} { if !strings.Contains(text, want) { t.Fatalf("the room at 44 columns lost %q:\n%s", want, text) } diff --git a/internal/tui3/provider_menu_test.go b/internal/tui3/provider_menu_test.go index d7ba77caad..9e86d13e6d 100644 --- a/internal/tui3/provider_menu_test.go +++ b/internal/tui3/provider_menu_test.go @@ -22,7 +22,7 @@ func TestDefaultProviderMenuUsesDefaultRefreshAndKeySetting(t *testing.T) { dir := t.TempDir() a := modelServiceTestApp(t, dir, "openai/gpt-4.1-mini", config.ResolveSources(dir, "test-key", ""), pickerCatalog) a.openSettings() - toProviders(t, a) + toConnections(t, a) calls := 0 a.refreshModels = func(context.Context) ([]Model, time.Time, error) { calls++ diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index 3243b30004..d3c94cb4e2 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -70,78 +70,28 @@ import ( // palette and nothing new: the panel is a list, and this surface already knows // what a list looks like (palette.go's overlayRow draws every row here). -// The tabs, in the order docs/CHAT-V3.md Decision 6 names them. +// Category labels are presentation only; persisted setting keys stay unchanged. const ( - // tabSession is this conversation and only this conversation: what it - // carries from the last one, and where it goes when its model will not - // answer. It is two rows, and that is the honest size of it — the models - // went to Providers, the money went to Spending, and the ssh link went to - // Workspace because it lands next launch rather than on this session. - tabSession = "Session" - // tabContext is what a model carries — the context law, whole. - tabContext = "Context" - // tabWorkspace is this machine and this project: what codeaf does with its - // own time here, and what it may reach on your behalf — a service it signs - // in to, and the ssh link it reaches another machine over. It is NOT where - // money lives any more, and that is the whole of - // docs/design/spending/DESIGN.md's first complaint — twenty rows answering - // four questions, with the dollar figures filed between `workers` and - // `memory floor`. - tabWorkspace = "Workspace" - // tabDisplay is how the surface draws itself and what it remembers of your - // typing. - tabDisplay = "Display" - // tabSpending is MONEY AND NOTHING ELSE: what codeaf may spend, per day, per - // conversation, per plan, and on its own practice — with what the day has - // actually cost at the top of it. It is the one editor money has, and every - // door on this surface that names a rail lands on one of its rows - // (settingspend.go). - tabSpending = "Spending" - // tabSafety is what codeaf may do without asking you first: the gate, its - // exceptions, the model that answers for you, and the two clocks that answer - // when nobody does. - tabSafety = "Safety" - // tabTasks is how work you can walk away from is run — how it starts, how it - // is checked, how much of it happens at once, and on whose hands. - tabTasks = "Tasks" - // tabTeams is what every team inherits when it says nothing of its own: - // who answers a member's question, what a team may spend in a day, what a - // new sub-team is given, and how deep teams may nest. A team's own - // overrides are on its card on the teams page; these are the defaults its - // `· from Settings` points at. - tabTeams = "Teams" - // tabProviders is which model answers what. - tabProviders = "Providers" + tabGeneral = "General" + tabProviders = "Models" + tabMemory = "Memory" + tabTasks = "Tasks" + tabTeams = "AI teams" + tabSafety = "Permissions" + tabSpending = "Spending" + tabPrivacy = "Privacy" + // Compatibility names keep existing internal entry points on their new category. + tabSession = tabMemory + tabContext = tabProviders + tabWorkspace = tabGeneral + tabDisplay = tabGeneral ) -// And the sixth, which is not a reading of the registry at all: the accounts -// this profile has connected and what each of them may do (connectcaps.go). It -// is last because the five before it are one object read five ways, and a -// person walking the bar meets the knobs before their accounts. -// Spending, Safety and Tasks stand between Display and Providers, and Spending -// leads the three: "what may it spend" is asked before "on which machine", and -// before either of the two questions that used to share its tab. -// -// Teams follows Tasks: both are about work you hand off, and its defaults are -// read after the question of how one task runs. -var settingTabs = []string{tabSession, tabContext, tabWorkspace, tabDisplay, - tabSpending, tabSafety, tabTasks, tabTeams, tabProviders, tabConnections} - -// settingTabCategory is the ONE-TO-ONE map between the four newer tabs and the -// four registry categories behind them, and it is the seam that keeps the skin -// honest about the one source of truth. -// -// The other tabs are a reading of the ROWS and not of the categories — "session -// ceiling" is a dollar figure that answers "what may THIS conversation do" — and -// that stays true of them. These four are different: the registry's own words -// for them (`spending`, `safety`, `tasks`, `teams`) are already the product's words for -// them, so a row that is filed under one and drawn under another would be two -// answers to one question. chrome_test.go pins the map in both directions. +var settingTabs = []string{tabGeneral, tabProviders, tabMemory, tabTasks, tabTeams, + tabSafety, tabSpending, tabConnections, tabPrivacy} var settingTabCategory = map[string]string{ - tabSpending: config.CategorySpending, - tabSafety: config.CategorySafety, - tabTasks: config.CategoryTasks, - tabTeams: config.CategoryTeams, + tabSpending: config.CategorySpending, tabSafety: config.CategorySafety, + tabTasks: config.CategoryTasks, tabTeams: config.CategoryTeams, } // settingWidget is how a row is ANSWERED, which is not quite how it reads. @@ -155,7 +105,7 @@ const ( widgetText settingWidget = iota // widgetToggle flips in place on enter or space. Booleans only. widgetToggle - // widgetCycle walks a short enum in place, in the registry's own order. + // widgetCycle opens the registry's short list of choices before saving. widgetCycle // widgetLane walks the four answers to "which machine behind this model" — // auto, pinned, pinned but borrowable, and openrouter. It is not @@ -868,15 +818,77 @@ func settingMetaFor(row config.Setting) (settingMeta, bool) { if !ok { return settingMeta{}, false } + presentation := row.ChatPresentation() + if presentation.Hidden { + return settingMeta{}, false + } + if presentation.Category != "" { + meta.tab = presentation.Category + } + if presentation.Label != "" { + meta.label = presentation.Label + } if meta.label == "" { meta.label = row.Label } + if presentation.Description != "" { + meta.about = presentation.Description + } if meta.about == "" { - meta.about = firstSentence(row.Hint) + meta.about = row.Hint } return meta, true } +// finishCategory keeps advanced controls discoverable without making the common +// path a catalog of engine details. Search deliberately bypasses this fold. +func (s *sheet) finishCategory() { + title := settingTabs[s.tab] + shown := make([]sheetItem, 0, len(s.items)) + if title == tabMemory { + shown = append(shown, sheetItem{memoryDoor: true, meta: settingMeta{label: "View saved memories", about: "Inspect, correct or remove saved memories."}}) + } + advanced := make([]sheetItem, 0) + catalog := make([]sheetItem, 0) + for at, item := range s.items { + if title == tabConnections && item.heading() && at+1 < len(s.items) && s.items[at+1].service != nil { + shown = append(shown, item) + continue + } + if title == tabConnections && (item.conn != nil || item.heading()) { + catalog = append(catalog, item) + continue + } + if item.row.Key != "" && item.row.ChatPresentation().Hidden { + continue + } + if item.heading() || item.row.ChatPresentation().Advanced || item.role != nil || item.autonomy != nil || item.crewDoor { + advanced = append(advanced, item) + } else if !item.heading() || title == tabConnections { + shown = append(shown, item) + } + } + // Lead with the controls a developer commonly changes, not registry order. + if title == tabTasks { + priority := map[string]int{config.KeyTaskParallel: 0, config.KeyTaskModel: 1, config.KeyTaskAudit: 2, config.KeyTaskSettle: 3, config.KeyTaskRepairRounds: 4} + rank := func(item sheetItem) int { + if n, ok := priority[item.row.Key]; ok { + return n + } + return len(priority) + } + sort.SliceStable(shown, func(i, j int) bool { return rank(shown[i]) < rank(shown[j]) }) + } + if len(advanced) > 0 { + shown = append(shown, sheetItem{advancedDoor: true, meta: settingMeta{label: "Advanced", about: "Less common controls. Search finds these even while collapsed."}}) + if s.advanced[title] { + shown = append(shown, advanced...) + } + } + s.items = append(shown, catalog...) + s.cursor = s.clampCursor(s.cursor) +} + // firstSentence is the registry hint cut to one line. A hint is written as // prose for a sheet with room; a panel row has one line and takes the sentence // that carries the meaning. @@ -896,9 +908,11 @@ const sheetRows = 16 // sheetItem is one line of the list: a row, or — while a search is on — the // faint tab heading a group of them sits under. type sheetItem struct { - head string - row config.Setting - meta settingMeta + advancedDoor bool + memoryDoor bool + head string + row config.Setting + meta settingMeta // conn is set on the rows of the Connections tab, which are accounts rather // than registry rows (connectcaps.go). It hangs here so that the cursor // walk, the scroll, the pointer and the hover need to know nothing about @@ -946,7 +960,8 @@ func (i sheetItem) restful() bool { return i.head == "" && i.read == nil } // sheet is the panel's whole state. The zero value is closed and costs the // frame nothing. type sheet struct { - tab int + advanced map[string]bool + tab int // host is the machine the session runs on over --host, "" otherwise. host string // teamDefaultsWrite says the teams seam can change that machine's `teams.` @@ -1056,8 +1071,9 @@ type sheet struct { // edit is the text submenu and sel the select submenu. At most one is open, // and while one is, it owns the keyboard. - edit *sheetEdit - sel *sheetSelect + edit *sheetEdit + sel *sheetSelect + choice *sheetChoice // today is the Spending tab's first row: what the day has cost, against what // it is allowed. It is TAKEN ONCE, when the panel opens (settingspend.go's @@ -1068,15 +1084,17 @@ type sheet struct { today *railReading // msg is the last refusal, in the registry's own words. - msg string + msg string + savedKey string // Set only after a setting write succeeds. } // sheetEdit is the one-line text submenu. type sheetEdit struct { - key string - label string - secret bool - box editor + pending bool // A remote write keeps the draft until its acknowledgement. + key string + label string + secret bool + box editor } // sheetEditNote is what the panel says ABOUT the box a value is being typed @@ -1098,6 +1116,11 @@ func sheetEditNote(label string, row config.Setting) string { return label } accepts := row.Accepts() + // Only optional counts with the registry's no-limit reading can make this + // promise; other blank values may restore a default or have another meaning. + if row.Kind == config.SettingCount && row.EmptyLabel == config.NoLimitWord { + accepts += " · blank for no limit" + } if accepts == "" { return label } @@ -1402,6 +1425,43 @@ func (s *sheet) searching() bool { return strings.TrimSpace(s.query.String()) != // THE FIRST MATCH: a person who typed three letters and found the thing has // already been told which tab it lives on, so backing the search out leaves // them there instead of back where they started. +// appendModelConnections keeps provider management available beside its API key +// after the category move, using the same actions as the connection picker. +func (s *sheet) appendModelConnections() { + if s.sources.Empty() { + return + } + // THE EMPTY PROFILE KEEPS THE DOOR AND DRAWS NOTHING ELSE: no services + // head, no connection row, no switcher (the emptiness test pins the + // absence of the section), because a row that could do nothing is + // decoration. The add row is an action, not decoration — a profile with + // no custom connection yet is the one that needs the door — so it stands + // alone when no service row stands beside it (customAddRow). + // THE EMPTY PROFILE KEEPS THE DOOR AND DRAWS NOTHING ELSE: no + // providers head, no default row, no connection rows — a row + // that could do nothing is decoration, and the add row stands + // alone when no service row stands beside it (the emptiness law). + services := modelServiceRows(s.profileDir, s.sources) + if len(services) > 0 { + s.items = append(s.items, sheetItem{head: "providers"}) + // THE DEFAULT PROVIDER LEADS: openrouter answers by default + // and is the row a person reads first. + if defaultRow := defaultServiceRow(s.profileDir, s.sources); defaultRow != nil { + s.items = append(s.items, sheetItem{service: defaultRow}) + } + for _, service := range services { + s.items = append(s.items, sheetItem{service: service}) + } + // THE SWITCHER RIDES BETWEEN THE CONNECTIONS AND THE DOOR: + // the add row is last, because the list reads as the + // providers you have, and the door to add another closes it. + if switcher := s.connectionSwitcherRow(); switcher != nil { + s.items = append(s.items, sheetItem{service: switcher}) + } + } + s.items = append(s.items, sheetItem{service: customAddRow()}) +} + func (s *sheet) build() { s.items = s.items[:0] // THE SEARCH'S SPAN STARTS EMPTY WITH THE LIST: every kept row's label @@ -1420,8 +1480,16 @@ func (s *sheet) build() { // the registry (connectcaps.go). It is one branch and no second list: what // it appends is [sheetItem]s, so everything downstream of here — the cursor, // the window, the pointer, the hover — is the code that was already there. - if s.onConnections() { + if s.onConnections() && query == "" { + for _, row := range s.tabRows() { + meta, _ := s.metaFor(row) + s.items = append(s.items, sheetItem{row: row, meta: meta}) + if row.Key == config.KeyAPIKey { + s.appendModelConnections() + } + } s.buildConnections() + s.finishCategory() return } if query == "" { @@ -1431,6 +1499,7 @@ func (s *sheet) build() { // (settingspend.go). if settingTabs[s.tab] == tabSpending { s.items = append(s.items, s.spendingItems()...) + s.finishCategory() s.cursor = s.clampCursor(s.cursor) return } @@ -1456,37 +1525,10 @@ func (s *sheet) build() { routingHeadDone = true } s.items = append(s.items, sheetItem{row: row, meta: meta}) - if row.Key == config.KeyAPIKey && !s.sources.Empty() { - // THE EMPTY PROFILE KEEPS THE DOOR AND DRAWS NOTHING ELSE: no services - // head, no connection row, no switcher (the emptiness test pins the - // absence of the section), because a row that could do nothing is - // decoration. The add row is an action, not decoration — a profile with - // no custom connection yet is the one that needs the door — so it stands - // alone when no service row stands beside it (customAddRow). - // THE EMPTY PROFILE KEEPS THE DOOR AND DRAWS NOTHING ELSE: no - // providers head, no default row, no connection rows — a row - // that could do nothing is decoration, and the add row stands - // alone when no service row stands beside it (the emptiness law). - services := modelServiceRows(s.profileDir, s.sources) - if len(services) > 0 { - s.items = append(s.items, sheetItem{head: "providers"}) - // THE DEFAULT PROVIDER LEADS: openrouter answers by default - // and is the row a person reads first. - if defaultRow := defaultServiceRow(s.profileDir, s.sources); defaultRow != nil { - s.items = append(s.items, sheetItem{service: defaultRow}) - } - for _, service := range services { - s.items = append(s.items, sheetItem{service: service}) - } - // THE SWITCHER RIDES BETWEEN THE CONNECTIONS AND THE DOOR: - // the add row is last, because the list reads as the - // providers you have, and the door to add another closes it. - if switcher := s.connectionSwitcherRow(); switcher != nil { - s.items = append(s.items, sheetItem{service: switcher}) - } - } - s.items = append(s.items, sheetItem{service: customAddRow()}) + if row.Key == config.KeyAPIKey { + s.appendModelConnections() } + // THE ROLES SECTION HANGS OFF THE ROW IT WRITES. Every pin those rows // set lands in "pinned roles" and nowhere else, so it is drawn // directly under it: a person reading one is reading the other, and a @@ -1504,12 +1546,16 @@ func (s *sheet) build() { s.items = append(s.items, s.autonomyItems(nil)...) } } + s.finishCategory() s.cursor = s.clampCursor(s.cursor) return } first := -1 + type resultGroup struct{ start, end, tab, score int } + groups := []resultGroup{} for tab, title := range settingTabs { start := len(s.items) + best := 0 // THE ROWS A TAB KEEPS ARE RANKED BY HOW WELL THEY ANSWERED, best first // and stable within equal scores on the registry's own order — so a // search reads as an answer key rather than as the panel reordered. The @@ -1525,6 +1571,7 @@ func (s *sheet) build() { if !hit { continue } + best = max(best, score) matched = append(matched, settingHit{row: row, meta: meta, score: score, hitAt: at, hitLen: n}) } sort.SliceStable(matched, func(a, b int) bool { return matched[a].score > matched[b].score }) @@ -1566,10 +1613,28 @@ func (s *sheet) build() { s.items = append(s.items, matched[1:]...) } } + if title == tabConnections { + s.buildConnections() + } + if len(s.items) > start { + groups = append(groups, resultGroup{start, len(s.items), tab, best}) + } if len(s.items) > start && first < 0 { first, s.tab = start+1, tab } } + // Put the category with the strongest answer first, keeping its rows together. + // A weak match in General must not bury an exact match in Permissions. + if len(groups) > 0 { + sort.SliceStable(groups, func(i, j int) bool { return groups[i].score > groups[j].score }) + ordered := make([]sheetItem, 0, len(s.items)) + for _, group := range groups { + ordered = append(ordered, s.items[group.start:group.end]...) + } + s.items = ordered + s.tab = groups[0].tab + first = 0 + } // Clamped rather than taken, because the first item of a group is a heading // and the second one can be a heading too: a search that matched only roles // opens on the tab's name, the section's name, and then a row. @@ -1698,6 +1763,12 @@ func (s *sheet) settingMatch(row config.Setting, meta settingMeta, tab string, t fields[2] = meta.about fields[3] = rowText(row) fields[4] = tab + presentation := row.ChatPresentation() + fields = append(fields, presentation.Aliases...) + fields = append(fields, settingValueWord(row)) + if old, ok := settingUI[row.Key]; ok { + fields = append(fields, old.label, old.tab) + } return s.matchHits(fields, terms) } @@ -2194,6 +2265,7 @@ func formatModelRoles(pins map[string]string) string { // yet starts the same browser trip /connect starts, and a sign-in is a thing // that reaches the network (connectcaps.go). func (a *app) sheetKey(msg tea.KeyPressMsg) (cmd tea.Cmd, took bool) { + a.sheet.savedKey = "" defer func() { if bind := a.takeSpendRailBindCmd(); bind != nil { cmd = tea.Batch(cmd, bind) @@ -2363,12 +2435,41 @@ func (s *sheet) tabBy(delta int) { // answers it — and on the Connections tab, open an account, walk one of its // answers, or start a sign-in, which is the one of them that needs a command. func (a *app) activate() tea.Cmd { + a.sheet.savedKey = "" s := &a.sheet item, ok := s.current() if !ok { return nil } + if item.memoryDoor { + return a.showPage(pageMemory) + } + if item.advancedDoor { + if s.advanced == nil { + s.advanced = map[string]bool{} + } + s.advanced[settingTabs[s.tab]] = !s.advanced[settingTabs[s.tab]] + s.build() + return nil + } if item.conn != nil { + // Service actions run on their category so asynchronous replies and Escape + // use the same account lifecycle as a service reached by browsing. + if s.searching() { + s.query.reset() + for at, title := range settingTabs { + if title == tabConnections { + s.tab = at + } + } + s.build() + for at, candidate := range s.items { + if candidate.conn != nil && candidate.conn.service == item.conn.service { + s.cursor = at + break + } + } + } return a.connAct(item.conn) } if item.crewDoor { @@ -2453,18 +2554,17 @@ func (a *app) activate() tea.Cmd { a.applySetting(item, next) case widgetCycle: - choices := item.row.Choices - if len(choices) == 0 { + if len(item.row.Choices) == 0 { return nil } - at := 0 - for i, choice := range choices { - if choice == item.row.Value() { - at = (i + 1) % len(choices) + choice := &sheetChoice{item: item, current: item.row.Value()} + for i, value := range item.row.Choices { + if value == choice.current { + choice.cursor = i break } } - a.applySetting(item, choices[at]) + s.choice = choice case widgetLane: // ENTER ON THIS ROW OPENS THE MACHINES. It used to walk four words — @@ -2622,7 +2722,8 @@ const gateNextSessionWord = "saved" + nextSessionWord // registry without ever writing it ([setupReviewKeys]) — and both run before this // window has a conversation to push a gate into. A push from there would be a // seam called with nothing on the other end. -func (a *app) applySetting(item sheetItem, raw string) { +func (a *app) applySetting(item sheetItem, raw string) bool { + a.sheet.savedKey = "" if item.row.Key == config.KeySpendRail && !a.railRead { // The panel's file write must not make the status line's first lazy // read claim an active limit the engine has not accepted yet. @@ -2630,16 +2731,9 @@ func (a *app) applySetting(item sheetItem, raw string) { } if err := item.row.Apply(raw); err != nil { a.sheet.msg = err.Error() - return - } - if item.row.Key == config.KeyWork { - a.workMode = config.WorkAt(a.profileDir) - a.touch() - } - if item.row.Key == config.KeyIcons { - a.adoptIcons() - a.touch() + return false } + a.refreshProfileUI() // THE TWO LANE ROWS LAND ON THE LIVE TRANSPORT, not at the next launch. // They are the panel's half of what the picker does when somebody pins from // it (lanes.go's laneRowChanged): a settings row that a person watched @@ -2712,13 +2806,16 @@ func (a *app) applySetting(item sheetItem, raw string) { } } a.sheet.msg = note + a.sheet.savedKey = item.row.Key a.sheet.rows = a.sheet.registry.Rows() a.sheet.build() + return true } // sheetEditKey drives the text submenu. enter saves, an empty box clears the // row, esc leaves it exactly as it was. func (a *app) sheetEditKey(msg tea.KeyPressMsg) tea.Cmd { + a.sheet.savedKey = "" s := &a.sheet edit := s.edit // The word and line jumps are the surface's, said once (editkeys.go). @@ -2733,10 +2830,14 @@ func (a *app) sheetEditKey(msg tea.KeyPressMsg) tea.Cmd { switch msg.String() { case "esc": s.edit = nil + s.msg = "" case "enter": + if edit.pending { + return nil + } + s.msg = "" row, ok := s.registry.Row(edit.key) raw := edit.box.String() - s.edit = nil if !ok { return nil } @@ -2745,7 +2846,9 @@ func (a *app) sheetEditKey(msg tea.KeyPressMsg) tea.Cmd { if cmd := a.writeHostTeamDefault(item, raw); cmd != nil || s.hostTeamRow(item) { return cmd } - a.applySetting(item, raw) + if a.applySetting(item, raw) { + s.edit = nil + } case "backspace": edit.box.deleteBackward() case "delete": @@ -2883,10 +2986,12 @@ func (a *app) readHostTeamDefaults() tea.Cmd { // A nil command with [sheet.hostTeamRow] false means this row is local and // the caller writes it the usual way. func (a *app) writeHostTeamDefault(item sheetItem, raw string) tea.Cmd { + a.sheet.savedKey = "" if !a.sheet.hostTeamRow(item) { return nil } if a.sheet.farTeams == nil { + a.sheet.msg = "waiting for this machine’s AI team defaults" return nil } door := a.teamsDisk.door.ApplyDefault @@ -2895,18 +3000,31 @@ func (a *app) writeHostTeamDefault(item sheetItem, raw string) tea.Cmd { return nil } key := item.row.Key + edit := a.sheet.edit + if edit != nil { + edit.pending = true + } return a.offLoop(func() func(bool) tea.Cmd { d, err := door(key, raw) return func(here bool) tea.Cmd { if !here || !a.at(pageSettings) { return nil } + if edit != nil && a.sheet.edit == edit { + edit.pending = false + } if err != nil { a.sheet.msg = err.Error() } else { copied := d a.sheet.farTeams = &copied a.sheet.msg = "" + a.sheet.savedKey = key + // Never close a replacement editor or discard text typed while the host + // was answering this earlier draft. + if edit != nil && a.sheet.edit == edit && edit.box.String() == raw { + a.sheet.edit = nil + } } a.sheet.build() a.touch() @@ -2921,6 +3039,7 @@ func (a *app) writeHostTeamDefault(item sheetItem, raw string) tea.Cmd { // the conversation. Everything else — the walk, the scroll, the filter — is // [picker.navigate], the same code /model runs. func (a *app) sheetSelectKey(msg tea.KeyPressMsg) { + a.sheet.savedKey = "" s := &a.sheet sel := s.sel switch msg.String() { @@ -3005,17 +3124,22 @@ const ( // sheetHitOption is one row of the select submenu; index is its position in // that submenu's own hits. sheetHitOption + sheetHitEditActions + sheetHitChoice + sheetHitChoiceCancel ) type sheetHit struct { - kind sheetHitKind - index int + category int // One-based sidebar category, zero outside its rows. + kind sheetHitKind + index int } // sheetPress is a click inside the panel: a tab word switches tabs, a row // selects and answers, anything else does nothing. It hands back a command for // the reason [app.sheetKey] does — a sign-in reaches the network. func (a *app) sheetPress(x, y int) (cmd tea.Cmd) { + a.sheet.savedKey = "" defer func() { if bind := a.takeSpendRailBindCmd(); bind != nil { cmd = tea.Batch(cmd, bind) @@ -3032,7 +3156,46 @@ func (a *app) sheetPress(x, y int) (cmd tea.Cmd) { if y < 0 || y >= len(hits) { return nil } - switch hit := hits[y]; hit.kind { + hit := hits[y] + if a.sheet.choice != nil { + switch hit.kind { + case sheetHitChoice: + a.sheet.choice.cursor = hit.index + cmd = a.saveSheetChoice() + case sheetHitChoiceCancel: + if x < 2 || x >= 12 { + return nil + } + cmd = a.choiceKey(tea.KeyPressMsg{Code: tea.KeyEscape}) + } + a.touch() + return cmd + } + if a.sheet.edit != nil { + if hit.kind != sheetHitEditActions { + return nil + } + switch sheetEditActionAt(x, width, a.sheet.edit.pending) { + case sheetEditSaveHover: + cmd = a.sheetEditKey(tea.KeyPressMsg{Code: tea.KeyEnter}) + case sheetEditCancelHover: + cmd = a.sheetEditKey(tea.KeyPressMsg{Code: tea.KeyEscape}) + } + a.touch() + return cmd + } + if a.sheet.sel != nil && hit.kind != sheetHitOption { + return nil + } + if a.sheetSidebar(width, height) && x < settingsSidebarWidth { + if hit.category > 0 && a.sheet.sel == nil && a.sheet.edit == nil { + a.sheet.tabBy(hit.category - 1 - a.sheet.tab) + a.saySettingsHost() + a.touch() + } + return nil + } + switch hit.kind { case sheetHitTabs: if tab, ok := tabAtColumn(x, width, a.sheet.tab); ok { if a.sheet.searching() { @@ -3077,11 +3240,42 @@ func (a *app) sheetPress(x, y int) (cmd tea.Cmd) { // sheetHover records which row the pointer is over, repainting only when the // answer changed (hover.go's rule, applied to the panel). -func (a *app) sheetHover(y int) { +func (a *app) sheetHover(y int) { a.sheetHoverAt(settingsSidebarWidth, y) } + +func (a *app) sheetHoverAt(x, y int) { width, height := a.size() _, hits, _, _ := a.sheetFrame(width, height) next := hoverAt{} - if y >= 0 && y < len(hits) && hits[y].kind == sheetHitRow { + if a.sheet.choice != nil { + if y >= 0 && y < len(hits) && hits[y].kind == sheetHitChoice { + next = hoverAt{kind: hoverSheet, index: hits[y].index} + } + if y >= 0 && y < len(hits) && hits[y].kind == sheetHitChoiceCancel && x >= 2 && x < 12 { + next = hoverAt{kind: hoverSheet, index: sheetChoiceCancelHover} + } + if next != a.hot { + a.hot = next + a.touch() + } + return + } + if a.sheet.edit != nil { + if y >= 0 && y < len(hits) && hits[y].kind == sheetHitEditActions { + action := sheetEditActionAt(x, width, a.sheet.edit.pending) + if action != 0 && !(action == sheetEditSaveHover && a.sheet.edit.pending) { + next = hoverAt{kind: hoverSheet, index: action} + } + } + if next != a.hot { + a.hot = next + a.touch() + } + return + } + if a.sheetSidebar(width, height) && x < settingsSidebarWidth && y >= 0 && y < len(hits) && hits[y].category > 0 { + next = hoverAt{kind: hoverSheet, index: -1 - hits[y].category} + } + if (!a.sheetSidebar(width, height) || x >= settingsSidebarWidth) && y >= 0 && y < len(hits) && hits[y].kind == sheetHitRow { next = hoverAt{kind: hoverSheet, index: hits[y].index} } if next == a.hot { @@ -3100,59 +3294,8 @@ func (a *app) sheetHover(y int) { // and the pointer resolves against them, and two answers to "where is the tab // bar" is how a click lands on the wrong tab. func (a *app) sheetFrame(width, height int) ([]string, []sheetHit, int, int) { - // THE HEAD AND THE FOOT BELONG TO THE ROUTER (pages.go). This panel's title - // row is gone — the place tab bar above says `settings` — and so is its keys - // line, which is the one hint every place shares now. ITS OWN TAB BAR STAYS, - // as the first row of its body, and the two bars are not a repetition: the - // nav at the top is the places and this one is this place's sections. - s := &a.sheet - pal := a.pal lines, hits, caretX, caretY := placeFrame(a, width, height, - func(width, room int) []placeRow { - rows := make([]placeRow, 0, room) - rows = append(rows, placeRow{text: sheetTabBar(width, s.tab, pal), hit: sheetHit{kind: sheetHitTabs}}) - rows = append(rows, placeRow{}) - room -= len(rows) - if room < 1 { - room = 1 - } - if s.sel != nil { - body, at := s.selectLines(width, room, pal, a.reasoningFor) - for i, line := range body { - hit := sheetHit{} - if at[i] >= 0 { - hit = sheetHit{kind: sheetHitOption, index: at[i]} - } - rows = append(rows, placeRow{text: line, hit: hit}) - } - return rows - } - body, owner := s.listLines(width, room, pal, a.hoveredSheetRow()) - at := s.cursorLine(owner) - // THE CURSOR'S ROW IS SCROLLED IN WHOLE. At [tierPhone] it is two - // lines — the name and the value under it — and a window that pinned - // only the first would push the value off the bottom edge, leaving a - // selection band with one end cut off and the fact being changed off - // screen. The last line is pinned first and the first line second, so - // a row taller than the window still shows its name. - if last := s.cursorLastLine(owner, at, width); last != at { - s.top = listTop(last, s.top, len(body), room) - } - s.top = listTop(at, s.top, len(body), room) - for i := 0; i < room; i++ { - index := s.top + i - if index >= len(body) { - rows = append(rows, placeRow{}) - continue - } - hit := sheetHit{} - if owner[index] >= 0 { - hit = sheetHit{kind: sheetHitRow, index: owner[index]} - } - rows = append(rows, placeRow{text: body[index], hit: hit}) - } - return rows - }) + func(width, room int) []placeRow { return (placeSettings{}).body(a, width, room) }) return lines, placeHitsOf(hits, sheetHit{}), caretX, caretY } @@ -3404,25 +3547,6 @@ func (s *sheet) listLines(width, room int, pal palette, hover int) ([]string, [] for _, line := range s.rowLinesWithin(item, i == s.cursor, i == hover, width, connBoxRows(room), pal) { put(line, i) } - if i != s.cursor { - continue - } - // THE ONE DESCRIPTION THIS PANEL EVER SHOWS is the selected row's, and - // on the accounts tab it is the selected SERVICE'S — a catalog row's own - // line about what connecting it buys, drawn under the row a person has - // stopped on and under no other. Most rows of that tab have none: a - // sentence explaining "read your mail" would be this surface saying the - // same thing twice (connectcaps.go's [connAbout]). - about := item.meta.about - if item.conn != nil { - about = connAbout(item.conn) - } - if about == "" { - continue - } - for _, line := range settingAboutLines(about, width) { - put(" "+pal.dim(line), i) - } } return lines, owner } @@ -3514,6 +3638,16 @@ func (s *sheet) rowLines(item sheetItem, selected, hovered bool, width int, pal // connection box. Ordinary rows do not spend it; the one tall row may use it // before dropping the end of a wrapped answer list. func (s *sheet) rowLinesWithin(item sheetItem, selected, hovered bool, width, boxRows int, pal palette) []string { + if item.memoryDoor { + return overlayLines(item.meta.label, "open", selected, false, hovered, width, pal) + } + if item.advancedDoor { + value := "show" + if s.advanced[settingTabs[s.tab]] { + value = "hide" + } + return overlayLines(item.meta.label, value, selected, false, hovered, width, pal) + } if item.conn != nil { return s.connRowLines(item.conn, selected, hovered, width, boxRows, pal) } @@ -3555,7 +3689,8 @@ func (s *sheet) rowLinesWithin(item sheetItem, selected, hovered bool, width, bo // The unit is the registry's ([config.Setting.Unit]), so it is stated once, // beside the default, rather than spelled again by every surface that draws // a number ([config.Setting.Reading]). - value := item.row.Reading() + value := settingValueWord(item.row) + if hosted, ok := s.hostedTeamReading(item); ok { value = hosted } @@ -3751,8 +3886,12 @@ func (s *sheet) footNote() string { func (s *sheet) keysLine() string { switch { + case s.edit != nil && s.edit.pending: + return "esc close · saving…" + case s.choice != nil: + return "↑↓ options · enter save · esc cancel" case s.edit != nil: - return "enter save · empty clears · esc cancel" + return "enter save · esc cancel" case s.sel != nil: // THE LEGEND SAYS `→ providers` ONLY WHERE `→` OPENS THEM — on a row that // has a provider row behind it. Offering the key on the drawing slot would @@ -3779,9 +3918,30 @@ func (s *sheet) keysLine() string { // says so on the row it works on and nowhere else — a line that offered // del everywhere would be offering it on rows where it does nothing. if item, ok := s.current(); ok && item.role != nil && item.role.pin != "" { - return "↑↓ move · enter pin · del unpin · type to search · esc close" + back := "esc close" + if s.searching() { + back = "esc clear search" + } + return "↑↓ move · enter pin · del unpin · type to search all · " + back + } + action := "change" + if item, ok := s.current(); ok { + switch { + case item.memoryDoor, item.crewDoor: + action = "open" + case item.advancedDoor: + action = "expand" + if s.advanced[settingTabs[s.tab]] { + action = "collapse" + } + case item.role != nil: + action = "choose model" + } + } + if s.searching() { + return "↑↓ results · enter " + action + " · esc clear search" } - return "↑↓ move · ←→ tabs · enter change · type to search · esc close" + return "↑↓ settings · ←→ categories · enter " + action + " · type to search all · esc close" } } @@ -3794,7 +3954,7 @@ func (a *app) sheetLayerOwnsKeys() bool { return false } s := &a.sheet - return s.edit != nil || s.sel != nil || s.conn.entry != nil + return s.edit != nil || s.sel != nil || s.choice != nil || s.conn.entry != nil } // hoveredSheetRow is the item the pointer is over, or -1. diff --git a/internal/tui3/settings_choice.go b/internal/tui3/settings_choice.go new file mode 100644 index 0000000000..1bd126336a --- /dev/null +++ b/internal/tui3/settings_choice.go @@ -0,0 +1,109 @@ +package tui3 + +import ( + tea "charm.land/bubbletea/v2" +) + +// sheetChoice keeps navigation separate from the persisted value. Opening or +// cancelling the list never invokes the registry writer. +const sheetChoiceCancelHover = -102 + +type sheetChoice struct { + item sheetItem + cursor int + current string +} + +func (a *app) choiceKey(msg tea.KeyPressMsg) tea.Cmd { + a.sheet.savedKey = "" + c := a.sheet.choice + if c == nil { + return nil + } + switch msg.String() { + case "esc": + a.sheet.choice = nil + a.sheet.msg = "" + case "up", "ctrl+p": + c.cursor = max(0, c.cursor-1) + case "down", "ctrl+n": + c.cursor = min(len(c.item.row.Choices)-1, c.cursor+1) + case "home": + c.cursor = 0 + case "end": + c.cursor = len(c.item.row.Choices) - 1 + case "enter", "space", " ": + return a.saveSheetChoice() + } + a.touch() + return nil +} + +func (a *app) saveSheetChoice() tea.Cmd { + c := a.sheet.choice + if c == nil || c.cursor < 0 || c.cursor >= len(c.item.row.Choices) { + return nil + } + if a.applySetting(c.item, c.item.row.Choices[c.cursor]) { + a.sheet.choice = nil + } + a.touch() + return nil +} + +func (s *sheet) choiceRows(width, room int, pal palette, hovered int) []placeRow { + c := s.choice + if room < 1 { + return nil + } + rows := []placeRow{{text: " " + placeHeading(fit(c.item.meta.label, max(1, width-4)), pal)}} + // Reserve one line per visible option and the Cancel action before adding + // explanation, so a small terminal cannot hide the selected answer. + optionRoom := max(1, room-3) + count := min(len(c.item.row.Choices), optionRoom) + top := max(0, min(c.cursor-count+1, len(c.item.row.Choices)-count)) + hint := c.item.row.ChatPresentation().Description + if hint == "" { + hint = c.item.row.Hint + } + hintRoom := max(0, room-count-4) + for _, line := range wrap(hint, max(1, width-4)) { + if hintRoom == 0 { + break + } + rows = append(rows, placeRow{text: " " + pal.dim(line)}) + hintRoom-- + } + if room-len(rows) > count+1 { + rows = append(rows, placeRow{}) + } + for i := top; i < top+count; i++ { + raw := c.item.row.Choices[i] + label := settingChoiceWord(c.item.row.Key, raw, raw) + current := "" + if raw == c.current { + current = " · current" + } + // Fit the label to keep each option a single pointer target even when narrow. + label = fit(label, max(1, width-16)) + current + lines := overlayLines(label, "", i == c.cursor, false, hovered == i, width, pal) + if len(lines) > 0 { + rows = append(rows, placeRow{text: lines[0], hit: sheetHit{kind: sheetHitChoice, index: i}}) + } + } + if s.msg != "" && len(rows) < room-1 { + rows = append(rows, placeRow{text: " " + pal.bad(fit(s.msg, max(1, width-4)))}) + } + cancel := pal.ink("Cancel") + if hovered == sheetChoiceCancelHover { + cancel = pal.underline(cancel) + } + rows = append(rows, placeRow{text: " " + cancel + pal.dim(" esc"), hit: sheetHit{kind: sheetHitChoiceCancel}}) + for _, line := range wrap(c.item.row.ChatPresentation().Activation, max(1, width-4)) { + if len(rows) >= room { + break + } + rows = append(rows, placeRow{text: " " + pal.dim(line)}) + } + return rows +} diff --git a/internal/tui3/settings_choice_test.go b/internal/tui3/settings_choice_test.go new file mode 100644 index 0000000000..b7af469316 --- /dev/null +++ b/internal/tui3/settings_choice_test.go @@ -0,0 +1,139 @@ +package tui3 + +import ( + tea "charm.land/bubbletea/v2" + "github.com/Agent-Field/codeaf/internal/config" + "strings" + "testing" +) + +// chooseSheetValue follows the displayed choices; no registry writes bypass the UI. +func chooseSheetValue(t *testing.T, a *app, value string) { + t.Helper() + drive(t, a, key("enter")) + if a.sheet.choice == nil { + t.Fatal("choice setting did not open options") + } + drive(t, a, key("home")) + found := false + for i, v := range a.sheet.choice.item.row.Choices { + if v == value { + for n := 0; n < i; n++ { + drive(t, a, key("down")) + } + found = true + break + } + } + if !found { + t.Fatalf("no option %q", value) + } + drive(t, a, key("enter")) +} + +func TestSettingsChoicePreviewCancelSaveAndReopen(t *testing.T) { + a, dir := sheetApp(t) + a.openSettings() + cursorTo(t, a, config.KeyWork) + row, _ := a.sheet.registry.Row(config.KeyWork) + before := row.Value() + drive(t, a, key("enter"), key("down")) + if a.sheet.choice == nil || config.WorkAt(dir) != before { + t.Fatal("opening or previewing wrote the value") + } + screen := plain(frame(a)) + if !strings.Contains(screen, "current") || !strings.Contains(screen, "collapsed") || !strings.Contains(screen, "expanded") { + t.Fatalf("choices do not explain the current and available values:\n%s", screen) + } + drive(t, a, key("esc")) + if a.sheet.choice != nil || config.WorkAt(dir) != before { + t.Fatal("cancel changed the value") + } + chooseSheetValue(t, a, "open") + if a.sheet.choice != nil || config.WorkAt(dir) != "open" { + t.Fatal("save did not persist chosen value") + } + drive(t, a, key("enter")) + if a.sheet.choice.current != "open" || a.sheet.choice.item.row.Choices[a.sheet.choice.cursor] != "open" { + t.Fatal("reopen did not mark the saved value") + } +} + +func TestSettingsChoiceMouseSaveAndCancel(t *testing.T) { + for _, width := range []int{80, 120} { + a, dir := sheetApp(t) + a.width = width + a.height = 35 + a.openSettings() + cursorTo(t, a, config.KeyWork) + drive(t, a, key("enter")) + _, hits, _, _ := a.sheetFrame(width, a.height) + target, cancel := -1, -1 + for y, hit := range hits { + if hit.kind == sheetHitChoice && a.sheet.choice.item.row.Choices[hit.index] == "open" { + target = y + } + if hit.kind == sheetHitChoiceCancel { + cancel = y + } + } + if target < 0 || cancel < 0 { + t.Fatal("missing option/cancel pointer targets") + } + before := config.WorkAt(dir) + a.sheetPress(3, cancel) + if a.sheet.choice != nil || config.WorkAt(dir) != before { + t.Fatal("mouse cancel wrote value") + } + drive(t, a, key("enter")) + a.sheetHoverAt(3, target) + if config.WorkAt(dir) != before { + t.Fatal("hover wrote value") + } + a.sheetPress(3, target) + if a.sheet.choice != nil || config.WorkAt(dir) != "open" { + t.Fatal("mouse save failed") + } + } +} + +func TestSettingsChoiceCompactKeepsSelectionAndCancel(t *testing.T) { + for _, width := range []int{24, 40, 80} { + a, _ := sheetApp(t) + a.width = width + a.height = 16 + a.openSettings() + cursorTo(t, a, config.KeyTaskStart) + drive(t, a, key("enter")) + screen := plain(frame(a)) + if !strings.Contains(screen, "current") || !strings.Contains(screen, "Cancel") { + t.Fatalf("%dc loses current or cancel:\n%s", width, screen) + } + drive(t, a, key("end")) + _, hits, _, _ := a.sheetFrame(width, a.height) + found := false + for _, h := range hits { + if h.kind == sheetHitChoice && h.index == a.sheet.choice.cursor { + found = true + } + } + if !found { + t.Fatalf("%dc hides selected option", width) + } + } +} + +func TestSettingsChoiceOwnsPasteAndWheel(t *testing.T) { + a, dir := sheetApp(t) + a.openSettings() + cursorTo(t, a, config.KeyWork) + before, query, cursor := config.WorkAt(dir), a.sheet.query.String(), a.sheet.cursor + drive(t, a, key("enter"), tea.PasteMsg{Content: "should not search"}) + drive(t, a, tea.MouseWheelMsg{X: 2, Y: 0, Button: tea.MouseWheelDown}) + if a.sheet.query.String() != query || a.sheet.cursor != cursor || !a.at(pageSettings) { + t.Fatal("modal input altered underlying settings") + } + if a.sheet.choice.cursor != len(a.sheet.choice.item.row.Choices)-1 || config.WorkAt(dir) != before { + t.Fatal("wheel failed to preview without saving") + } +} diff --git a/internal/tui3/settings_editor_actions_test.go b/internal/tui3/settings_editor_actions_test.go new file mode 100644 index 0000000000..cbe8aae8b7 --- /dev/null +++ b/internal/tui3/settings_editor_actions_test.go @@ -0,0 +1,134 @@ +package tui3 + +import ( + "github.com/Agent-Field/codeaf/internal/config" + "strings" + "testing" +) + +func TestSettingsEditorActionsSaveCancelAndValidation(t *testing.T) { + for _, size := range [][2]int{{80, 30}, {120, 30}, {40, 16}, {40, 12}} { + a, dir := sheetApp(t) + a.width, a.height = size[0], size[1] + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + actionRow := func() int { + t.Helper() + lines, hits, x, y := a.sheetFrame(a.width, a.height) + if y < 0 || y >= len(lines) || x < 0 { + t.Fatal("editor caret absent") + } + for at, hit := range hits { + if hit.kind == sheetHitEditActions { + if !strings.Contains(plain(lines[at]), "Save") || !strings.Contains(plain(lines[at]), "Cancel") { + t.Fatal("invisible actions") + } + return at + } + } + t.Fatalf("actions absent at %v:\n%s", size, plain(strings.Join(lines, "\n"))) + return -1 + } + y := actionRow() + a.sheet.edit.box.setText("7") + a.sheetPress(15, y) + if a.sheet.edit == nil || config.TaskParallelAt(dir) != 0 { + t.Fatal("blank action gap saved") + } + a.sheetHoverAt(3, y) + if a.hot.kind != hoverSheet || a.hot.index != sheetEditSaveHover { + t.Fatal("Save has no hover") + } + a.sheetPress(18, y) + if a.sheet.edit != nil || config.TaskParallelAt(dir) != 0 { + t.Fatal("Cancel saved draft") + } + a.activate() + a.sheet.edit.box.setText("invalid") + a.sheetPress(3, actionRow()) + if a.sheet.edit == nil || a.sheet.msg == "" || config.TaskParallelAt(dir) != 0 { + t.Fatal("click save skipped validation") + } + a.sheet.edit.box.setText("4") + a.sheetPress(3, actionRow()) + if a.sheet.edit != nil || config.TaskParallelAt(dir) != 4 { + t.Fatal("click Save failed to persist") + } + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + if a.sheet.edit.box.String() != "4" { + t.Fatal("saved value did not reopen") + } + a.sheet.edit.box.setText("8") + a.sheetEditKey(key("esc")) + if config.TaskParallelAt(dir) != 4 { + t.Fatal("keyboard Cancel saved") + } + } +} + +func TestSettingsEditorShowsRestartBesideField(t *testing.T) { + a, _ := sheetApp(t) + a.width, a.height = 80, 30 + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + lines, _, _, _ := a.sheetFrame(a.width, a.height) + frame := plain(strings.Join(lines, "\n")) + if !strings.Contains(frame, "Restart the CLI") || !strings.Contains(frame, "blank for no limit") { + t.Fatalf("missing field context:\n%s", frame) + } +} + +func TestSettingsEditorPendingWriteOffersClose(t *testing.T) { + a, dir := sheetApp(t) + a.width, a.height = 80, 30 + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + a.sheet.host = "build-host" + a.sheet.edit.pending = true + a.sheet.edit.box.setText("9") + lines, hits, _, _ := a.sheetFrame(a.width, a.height) + frame := plain(strings.Join(lines, "\n")) + if !strings.Contains(frame, "Saving on build-host") || !strings.Contains(frame, "Close") || strings.Contains(frame, "Cancel") { + t.Fatalf("pending write implies cancellation:\n%s", frame) + } + for y, h := range hits { + if h.kind == sheetHitEditActions { + a.sheetPress(3, y) + if a.sheet.edit == nil || config.TaskParallelAt(dir) != 0 { + t.Fatal("pending Save reissued write") + } + a.sheetPress(18, y) + if a.sheet.edit != nil { + t.Fatal("pending Close did not close editor") + } + return + } + } + t.Fatal("pending actions absent") +} + +func TestSettingsEditorNarrowActionBounds(t *testing.T) { + a, _ := sheetApp(t) + a.width, a.height = 24, 24 + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + lines, hits, _, _ := a.sheetFrame(a.width, a.height) + for y, h := range hits { + if h.kind == sheetHitEditActions { + if !strings.Contains(plain(lines[y]), "Save Cancel") { + t.Fatalf("narrow labels: %s", plain(lines[y])) + } + a.sheetPress(10, y) + if a.sheet.edit != nil { + t.Fatal("narrow Cancel did not work") + } + return + } + } + t.Fatal("narrow actions absent") +} diff --git a/internal/tui3/settings_live.go b/internal/tui3/settings_live.go new file mode 100644 index 0000000000..ae25593b69 --- /dev/null +++ b/internal/tui3/settings_live.go @@ -0,0 +1,17 @@ +package tui3 + +import "github.com/Agent-Field/codeaf/internal/config" + +// refreshProfileUI adopts presentation preferences after a panel save or a chat +// turn. Keep temporary layout choices (such as the task column) and the engine's +// approval posture out: neither is a profile-only display preference. +func (a *app) refreshProfileUI() { + a.mouse = config.MouseEnabledAt(a.profileDir) + a.timestamps = config.TimestampsAt(a.profileDir) + a.workMode = config.WorkAt(a.profileDir) + a.adoptIcons() + a.hopQuick = config.QuickSwitchAt(a.profileDir) + a.askWait = a.consentWait() + a.notices.enabled = config.HintsAt(a.profileDir) + a.touch() +} diff --git a/internal/tui3/settings_live_test.go b/internal/tui3/settings_live_test.go new file mode 100644 index 0000000000..7f587dac77 --- /dev/null +++ b/internal/tui3/settings_live_test.go @@ -0,0 +1,82 @@ +package tui3 + +import ( + "testing" + + "github.com/Agent-Field/codeaf/internal/config" +) + +func TestSettingsLivePanelRefreshesPresentation(t *testing.T) { + cases := []struct { + key, raw string + check func(*app) bool + }{ + {config.KeyMouse, "off", func(a *app) bool { return !a.mouse }}, + {config.KeyTimestamps, "footers", func(a *app) bool { return a.stampsOn() }}, + {config.KeyWork, "open", func(a *app) bool { return a.workMode == "open" }}, + {config.KeyIcons, "plain", func(a *app) bool { return a.iconMode == config.IconsPlain }}, + {config.KeyQuickSwitch, "off", func(a *app) bool { return !a.hopQuick }}, + // The old registry row is "hide hints"; the TUI presents its inverse. + {config.KeyHints, "on", func(a *app) bool { return !a.notices.enabled }}, + } + for _, tc := range cases { + t.Run(tc.key, func(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + a.mouse, a.hopQuick, a.notices.enabled = true, true, true + a.workMode = "fold" + a.timestamps = config.TimestampsOff + a.iconMode = config.IconsAuto + a.railAway = true // A temporary keyboard choice must survive a preference save. + a.dirty = false + row, ok := a.sheet.registry.Row(tc.key) + if !ok { + t.Fatalf("missing %s", tc.key) + } + meta, _ := settingMetaFor(row) + if !a.applySetting(sheetItem{row: row, meta: meta}, tc.raw) { + t.Fatal(a.sheet.msg) + } + if !tc.check(a) || !a.dirty { + t.Fatal("saved setting did not refresh the visible presentation immediately") + } + if !a.railAway { + t.Fatal("preference refresh reopened the manually hidden task column") + } + }) + } +} + +func TestSettingsLiveTurnCompletionAdoptsToolProfileWrites(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + registry := a.sheet.registry + a.closeSettings() + a.mouse, a.hopQuick, a.notices.enabled = true, true, true + a.workMode = "fold" + a.timestamps = config.TimestampsOff + a.railAway = true + for _, change := range [][2]string{ + {config.KeyMouse, "off"}, + {config.KeyTimestamps, "footers"}, + {config.KeyWork, "open"}, + {config.KeyQuickSwitch, "off"}, + {config.KeyHints, "on"}, + } { + row, ok := registry.Row(change[0]) + if !ok { + t.Fatal(change[0]) + } + if err := row.Apply(change[1]); err != nil { + t.Fatal(err) + } + } + // A chat tool writes the registry; its completed turn is the refresh boundary. + a.settle() + if a.mouse || a.hopQuick || a.notices.enabled || a.workMode != "open" || !a.stampsOn() { + t.Fatal("turn completion did not adopt saved profile preferences") + } + if !a.railAway { + t.Fatal("turn refresh discarded the manually hidden task column") + } +} diff --git a/internal/tui3/settings_organization_test.go b/internal/tui3/settings_organization_test.go new file mode 100644 index 0000000000..3c429a1a5d --- /dev/null +++ b/internal/tui3/settings_organization_test.go @@ -0,0 +1,354 @@ +package tui3 + +import ( + "strings" + "testing" + + "github.com/Agent-Field/codeaf/internal/config" +) + +func TestSettingsOrganizationCategoriesAndAdvancedSearch(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + if settingTabs[a.sheet.tab] != tabGeneral { + t.Fatal("settings must open on General") + } + want := "General,Models,Memory,Tasks,AI teams,Permissions,Spending,Connections,Privacy" + if strings.Join(settingTabs, ",") != want { + t.Fatalf("categories: %v", settingTabs) + } + for tab := range settingTabs { + a.sheet.tab = tab + a.sheet.build() + for _, item := range a.sheet.items { + if item.row.Key != "" && (item.row.ChatPresentation().Advanced || item.row.ChatPresentation().Hidden) { + t.Fatalf("collapsed category shows %s", item.row.Key) + } + } + } + a.sheet.query.setText(config.KeyPromptProfile) + a.sheet.build() + found := false + for _, item := range a.sheet.items { + found = found || item.row.Key == config.KeyPromptProfile + } + if !found { + t.Fatal("global search must find a collapsed advanced setting by its existing key") + } +} + +func TestSettingsOrganizationSidebarHitTargets(t *testing.T) { + a, _ := sheetApp(t) + a.width = 120 + a.height = 35 + a.openSettings() + _, hits, _, _ := a.sheetFrame(a.width, a.height) + target := -1 + for y, hit := range hits { + if hit.category == 3 { + target = y + break + } + } + if target < 0 { + t.Fatal("Memory category has no pointer target") + } + a.sheetPress(3, target) + if settingTabs[a.sheet.tab] != tabMemory { + t.Fatal("sidebar click did not select Memory") + } + cursor := a.sheet.cursor + a.sheetHoverAt(3, target) + if a.sheet.cursor != cursor || a.hoveredSheetRow() >= 0 { + t.Fatal("category hover changed or previewed a setting") + } + a.width = 80 + _, hits, _, _ = a.sheetFrame(a.width, a.height) + for _, hit := range hits { + if hit.category > 0 { + t.Fatal("compact frame retains sidebar targets") + } + } + if !strings.Contains(plain(frame(a)), tabMemory) { + t.Fatal("compact bar lost selected category") + } +} + +func TestSettingsOrganizationHintsPreserveExistingValue(t *testing.T) { + a, dir := sheetApp(t) + a.openSettings() + a.sheet.query.setText(config.KeyHints) + a.sheet.build() + item, ok := a.sheet.current() + if !ok || item.row.Key != config.KeyHints { + t.Fatal("hints search failed") + } + before := item.row.Value() + a.activate() + rows := config.NewSettings(config.SettingsOptions{ProfileDir: dir}).Rows() + for _, row := range rows { + if row.Key == config.KeyHints { + if row.Value() == before { + t.Fatal("toggle did not persist") + } + item.row = row + shown := plain(strings.Join(a.sheet.rowLines(item, true, false, 80, a.pal), "\n")) + positive := "on" + if row.Value() == "on" { + positive = "off" + } + if !strings.Contains(shown, positive) { + t.Fatalf("positive hint value absent: %s", shown) + } + return + } + } + t.Fatal("persisted hint row missing") +} + +func TestSettingsOrganizationConnectionSearchFromEveryCategory(t *testing.T) { + a, _ := capsApp(t, twoAccounts) + for tab := range settingTabs { + a.sheet.tab = tab + a.sheet.query.setText("Slack") + a.sheet.build() + found := false + for _, item := range a.sheet.items { + found = found || (item.conn != nil && item.conn.service == "slack") + } + if !found { + t.Fatalf("Slack cannot be found from %s", settingTabs[tab]) + } + } +} + +func TestSettingsOrganizationCaretAndMemoryDoor(t *testing.T) { + a, _ := sheetApp(t) + a.width = 120 + a.height = 35 + a.openSettings() + a.sheet.query.setText("mouse") + a.sheet.build() + _, _, x, y := a.sheetFrame(a.width, a.height) + if x < settingsSidebarWidth || y < 0 || !a.caret { + t.Fatalf("sidebar search caret misplaced: %d,%d visible=%v", x, y, a.caret) + } + a.sheet.query.reset() + for i, title := range settingTabs { + if title == tabMemory { + a.sheet.tab = i + } + } + a.sheet.build() + if len(a.sheet.items) == 0 || !a.sheet.items[0].memoryDoor { + t.Fatal("Memory has no inspect door") + } + a.sheet.cursor = 0 + a.activate() + if !a.at(pageMemory) { + t.Fatal("inspect door did not open saved memories") + } +} + +func TestSettingsOrganizationRenderedDetailsAndPinnedScope(t *testing.T) { + a, _ := sheetApp(t) + a.width, a.height = 80, 30 + t.Setenv("CODEAF_DAILY_BUDGET", "22") + a.openSettings() + cursorTo(t, a, config.KeyMemoryEnabled) + visible := strings.Join(strings.Fields(plain(frame(a))), " ") + for _, want := range []string{"Restart the CLI to apply this to already-open chats", "Turning this off does not delete saved memories."} { + if !strings.Contains(visible, want) { + t.Fatalf("critical memory detail absent from rendered 80-column frame: %s\n%s", want, visible) + } + } + cursorTo(t, a, config.KeyDailyBudget) + visible = strings.Join(strings.Fields(plain(frame(a))), " ") + if !strings.Contains(visible, "held by CODEAF_DAILY_BUDGET") || !strings.Contains(visible, "unset it to change this here") { + t.Fatalf("pinned scope remedy absent: %s", visible) + } +} + +func TestSettingsOrganizationHoverKeepsRenderedTargetsStable(t *testing.T) { + a, _ := sheetApp(t) + a.width, a.height = 80, 30 + a.openSettings() + cursorTo(t, a, config.KeyHints) + _, before, _, _ := a.sheetFrame(a.width, a.height) + hovered := -1 + for y, hit := range before { + if hit.kind == sheetHitRow && hit.index != a.sheet.cursor { + hovered = y + break + } + } + if hovered < 0 { + t.Fatal("no second visible row") + } + a.sheetHoverAt(3, hovered) + _, after, _, _ := a.sheetFrame(a.width, a.height) + for y, hit := range before { + if hit.kind == sheetHitRow && (after[y].kind != hit.kind || after[y].index != hit.index) { + t.Fatalf("hover moved target at row %d", y) + } + } +} + +func TestSettingsOrganizationEditorsOwnPointer(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + cursorTo(t, a, config.KeyDailyBudget) + a.activate() + if a.sheet.edit == nil { + t.Fatal("budget did not open editor") + } + tab := a.sheet.tab + _, hits, _, _ := a.sheetFrame(a.width, a.height) + for y, hit := range hits { + if hit.kind == sheetHitTabs || hit.kind == sheetHitRow { + a.sheetPress(2, y) + } + } + if a.sheet.tab != tab || a.sheet.edit == nil { + t.Fatal("underlying click escaped text editor") + } + a.sheet.edit = nil + cursorTo(t, a, config.ModelSettingKey(talkSlot)) + a.activate() + if a.sheet.sel == nil { + t.Fatal("chat model did not open picker") + } + tab = a.sheet.tab + _, hits, _, _ = a.sheetFrame(a.width, a.height) + for y, hit := range hits { + if hit.kind == sheetHitTabs { + a.sheetPress(2, y) + } + } + if a.sheet.tab != tab || a.sheet.sel == nil { + t.Fatal("underlying category click escaped model picker") + } +} + +func TestSettingsOrganizationInvalidEditKeepsDraft(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + row, _ := a.sheet.registry.Row(config.KeyTaskParallel) + before := row.Value() + a.activate() + if a.sheet.edit == nil { + t.Fatal("integer setting did not open editor") + } + a.sheet.edit.box.setText("not-a-number") + a.sheetEditKey(key("enter")) + if a.sheet.edit == nil || a.sheet.edit.box.String() != "not-a-number" { + t.Fatal("validation discarded the editable draft") + } + if row.Value() != before { + t.Fatal("invalid input changed persisted setting") + } + if a.sheet.msg == "" || !strings.Contains(plain(frame(a)), a.sheet.msg) { + t.Fatalf("validation error not visible while editor is open: %q", a.sheet.msg) + } + a.sheet.edit.box.setText("3") + a.sheetEditKey(key("enter")) + if a.sheet.edit != nil || row.Value() != "3" { + t.Fatal("correcting the draft did not save and close") + } + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + a.sheet.edit.box.setText("4") + a.sheetEditKey(key("esc")) + if a.sheet.edit != nil || row.Value() != "3" || a.sheet.msg != "" { + t.Fatal("Escape did not cancel draft cleanly") + } +} + +func TestSettingsOrganizationRenderedEditorDraftAndCaret(t *testing.T) { + for _, width := range []int{80, 120} { + t.Run(string(rune(width)), func(t *testing.T) { + a, _ := sheetApp(t) + a.width, a.height = width, 30 + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + a.sheet.edit.box.setText("not-a-number") + a.sheetEditKey(key("enter")) + lines, _, x, y := a.sheetFrame(a.width, a.height) + if !a.caret || y < 0 || y >= len(lines) || !strings.Contains(plain(lines[y]), "not-a-number") || x <= 2 { + t.Fatalf("draft or caret absent at %d: %d,%d\n%s", width, x, y, plain(strings.Join(lines, "\n"))) + } + a.sheet.edit.box.setText("3") + lines, _, _, y = a.sheetFrame(a.width, a.height) + if !strings.Contains(plain(lines[y]), "3") { + t.Fatal("corrected draft is invisible") + } + a.sheetEditKey(key("esc")) + cursorTo(t, a, config.KeyExaKey) + a.activate() + a.sheet.edit.box.setText("exa-private-value") + lines, _, end, y := a.sheetFrame(a.width, a.height) + if strings.Contains(plain(strings.Join(lines, "\n")), "exa-private-value") { + t.Fatal("credential leaked in editor frame") + } + a.sheet.edit.box.left() + _, _, left, newY := a.sheetFrame(a.width, a.height) + if !a.caret || y != newY || left >= end { + t.Fatal("masked editor caret does not follow cursor movement") + } + }) + } +} + +func TestSettingsOrganizationCountEditorInlineValidationAndClearing(t *testing.T) { + for _, size := range []struct { + name string + width int + }{{"narrow", 80}, {"wide", 120}} { + t.Run(size.name, func(t *testing.T) { + a, dir := sheetApp(t) + a.width, a.height = size.width, 30 + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + a.sheet.edit.box.setText("invalid") + a.sheetEditKey(key("enter")) + if a.sheet.edit == nil || config.TaskParallelAt(dir) != 0 { + t.Fatal("invalid draft changed stored count or closed editor") + } + lines, _, _, y := a.sheetFrame(a.width, a.height) + if !strings.Contains(plain(lines[y]), "invalid") || y+1 >= len(lines) || !strings.Contains(plain(lines[y+1]), "not a whole number") { + t.Fatalf("error is not directly below input:\n%s", plain(strings.Join(lines, "\n"))) + } + frame := plain(strings.Join(lines, "\n")) + if strings.Count(frame, "not a whole number") != 1 || !strings.Contains(frame, "blank for no limit") { + t.Fatalf("missing blank semantics or duplicate error:\n%s", frame) + } + a.sheet.edit.box.setText("4") + a.sheetEditKey(key("enter")) + if a.sheet.edit != nil || config.TaskParallelAt(dir) != 4 { + t.Fatal("valid count was not saved") + } + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + if a.sheet.edit == nil || a.sheet.edit.box.String() != "4" { + t.Fatal("reopened count did not retain 4") + } + a.sheet.edit.box.reset() + a.sheetEditKey(key("enter")) + if a.sheet.edit != nil || config.TaskParallelAt(dir) != 0 { + t.Fatal("blank did not remove the count cap") + } + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + if a.sheet.edit == nil || a.sheet.edit.box.String() != "" { + t.Fatal("unlimited count did not reopen as blank") + } + }) + } + ordinary := config.Setting{Kind: config.SettingCount, Label: "repair attempts"} + if strings.Contains(sheetEditNote("repair attempts", ordinary), "blank for no limit") { + t.Fatal("nonoptional count promises incorrect blank semantics") + } +} diff --git a/internal/tui3/settings_receipt_test.go b/internal/tui3/settings_receipt_test.go new file mode 100644 index 0000000000..66bbeb3894 --- /dev/null +++ b/internal/tui3/settings_receipt_test.go @@ -0,0 +1,36 @@ +package tui3 + +import ( + "github.com/Agent-Field/codeaf/internal/config" + "strings" + "testing" +) + +func TestSettingsReceiptOnlyAfterSuccessfulWrite(t *testing.T) { + a, dir := sheetApp(t) + a.openSettings() + cursorTo(t, a, config.KeyTaskParallel) + a.activate() + a.sheet.edit.box.setText("bad") + a.sheetEditKey(key("enter")) + if a.sheet.savedKey != "" || config.TaskParallelAt(dir) != 0 { + t.Fatal("failed save claimed success") + } + a.sheet.edit.box.setText("4") + a.sheetEditKey(key("enter")) + if a.sheet.savedKey != config.KeyTaskParallel || config.TaskParallelAt(dir) != 4 { + t.Fatal("successful save missing receipt") + } + if !strings.Contains(plain(strings.Join((placeSettings{}).note(a, 80), "")), "Saved") { + t.Fatal("receipt not rendered") + } + a.activate() + if a.sheet.savedKey != "" { + t.Fatal("old receipt remained while editing") + } + a.sheet.edit.box.setText("8") + a.sheetEditKey(key("esc")) + if a.sheet.savedKey != "" || config.TaskParallelAt(dir) != 4 { + t.Fatal("cancel claimed save") + } +} diff --git a/internal/tui3/settings_words.go b/internal/tui3/settings_words.go new file mode 100644 index 0000000000..c3493110e6 --- /dev/null +++ b/internal/tui3/settings_words.go @@ -0,0 +1,11 @@ +package tui3 + +import "github.com/Agent-Field/codeaf/internal/config" + +// settingValueWord changes only the reading; persisted values remain unchanged. +func settingValueWord(row config.Setting) string { + return config.ChatChoiceLabel(row.Key, row.Value(), row.Reading()) +} +func settingChoiceWord(key, raw, fallback string) string { + return config.ChatChoiceLabel(key, raw, fallback) +} diff --git a/internal/tui3/settings_words_test.go b/internal/tui3/settings_words_test.go new file mode 100644 index 0000000000..a528b49add --- /dev/null +++ b/internal/tui3/settings_words_test.go @@ -0,0 +1,121 @@ +package tui3 + +import ( + "github.com/Agent-Field/codeaf/internal/config" + "strings" + "testing" +) + +func TestSettingsWordsKeepSavedChoicesAndSearchableReadings(t *testing.T) { + for _, test := range []struct{ key, raw, shown string }{ + {config.KeyTaskStart, "sized", "assess while working"}, + {config.KeyTaskStart, "single", "skip assessment"}, + {config.KeyTaskSettle, "auto", "let the chat decide"}, + {config.KeyToolApprovalMode, "prompt", "ask by default"}, + {config.KeyWork, "fold", "collapsed"}, + {config.KeyModelPool, "read", "use only"}, + } { + t.Run(test.key+test.raw, func(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + cursorTo(t, a, test.key) + item, _ := a.sheet.current() + if err := item.row.Apply(test.raw); err != nil { + t.Fatal(err) + } + if got := item.row.Value(); got != test.raw { + t.Fatalf("saved choice %q", got) + } + if got := settingValueWord(item.row); got != test.shown { + t.Fatalf("reading %q, want %q", got, test.shown) + } + for _, query := range []string{test.raw, test.shown} { + if itemAt(searchItems(t, a, query), test.key) < 0 { + t.Fatalf("cannot find %s via %q", test.key, query) + } + } + }) + } +} + +func TestSettingsSearchAndHelpNameTheirCurrentScope(t *testing.T) { + a, _ := sheetApp(t) + a.width, a.height = 120, 35 + a.openSettings() + a.raiseSettings() + if !strings.Contains(a.sheet.keysLine(), "esc close") { + t.Fatal("resting Escape must say close") + } + typeQuery(t, a, "mouse") + if !strings.Contains(plain(frame(a)), "Search results") { + t.Fatal("global results still look like one category") + } + if !strings.Contains(a.sheet.keysLine(), "esc clear search") { + t.Fatal("search Escape must name what it clears") + } + drive(t, a, key("esc")) + if !a.at(pageSettings) || a.sheet.searching() { + t.Fatal("clearing search closed settings") + } +} + +func TestSettingsActionHintsDescribeDoorsWithoutClaimingAWrite(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + for i, name := range settingTabs { + if name == tabMemory { + a.sheet.tab = i + } + } + a.sheet.build() + if !strings.Contains(a.sheet.keysLine(), "enter open") { + t.Fatal("memory door claims to change a setting") + } + if strings.Contains(strings.Join((placeSettings{}).note(a, 80), " "), "saved") { + t.Fatal("inspection claims to write the profile") + } + for i, name := range settingTabs { + if name == tabTasks { + a.sheet.tab = i + } + } + a.sheet.build() + for i, item := range a.sheet.items { + if item.advancedDoor { + a.sheet.cursor = i + break + } + } + if !strings.Contains(a.sheet.keysLine(), "enter expand") { + t.Fatal("Advanced does not name expansion") + } + a.activate() + if !strings.Contains(a.sheet.keysLine(), "enter collapse") { + t.Fatal("expanded Advanced does not name collapse") + } +} + +func TestTasksLeadsWithEverydayControlsAndKeepsTuningSearchable(t *testing.T) { + a, _ := sheetApp(t) + a.openSettings() + for i, name := range settingTabs { + if name == tabTasks { + a.sheet.tab = i + } + } + a.sheet.build() + want := []string{config.KeyTaskParallel, config.KeyTaskModel, config.KeyTaskAudit} + for i, key := range want { + if a.sheet.items[i].row.Key != key { + t.Fatalf("task row %d = %s, want %s", i, a.sheet.items[i].row.Key, key) + } + } + for _, item := range a.sheet.items { + if item.row.Key == config.KeyTaskStart || item.row.Key == config.KeyBashBackgroundAfter { + t.Fatal("analysis or timing tuning escaped Advanced") + } + } + if itemAt(searchItems(t, a, config.KeyTaskStart), config.KeyTaskStart) < 0 { + t.Fatal("task analysis cannot be found while Advanced is collapsed") + } +} diff --git a/internal/tui3/settingsautonomy_test.go b/internal/tui3/settingsautonomy_test.go index 22787fb4ca..d71dec6625 100644 --- a/internal/tui3/settingsautonomy_test.go +++ b/internal/tui3/settingsautonomy_test.go @@ -13,6 +13,10 @@ func toSafety(t *testing.T, a *app) { for at, title := range settingTabs { if title == tabSafety { a.sheet.tab = at + if a.sheet.advanced == nil { + a.sheet.advanced = map[string]bool{} + } + a.sheet.advanced[tabSafety] = true a.sheet.build() return } diff --git a/internal/tui3/settingsfix_test.go b/internal/tui3/settingsfix_test.go index 90905fb472..ffe60ea1b8 100644 --- a/internal/tui3/settingsfix_test.go +++ b/internal/tui3/settingsfix_test.go @@ -225,15 +225,14 @@ func TestTheSearchingRowFollowsProviderAndKeyWrites(t *testing.T) { t.Fatalf("the untouched searching row reads:\n%s", screen) } - // V5: the first press is the safe keyless default, not a keyed pin. - drive(t, a, key("enter")) + // V5: choose the safe keyless provider explicitly. + chooseSheetValue(t, a, "firecrawl") row, _ := a.sheet.registry.Row(config.KeySearchProvider) if got := row.Value(); got != "firecrawl" { - t.Fatalf("one cycle from auto landed on %q, want firecrawl", got) + t.Fatalf("choosing firecrawl landed on %q, want firecrawl", got) } - // The remaining order is duckduckgo, exa, which reaches the broken-pin - // explanation through the same cycle a person uses. - drive(t, a, key("enter"), key("enter")) + // Choosing the keyed provider exposes its missing-key explanation. + chooseSheetValue(t, a, "exa") if screen := plain(frame(a)); !strings.Contains(strings.Join(strings.Fields(screen), " "), "exa is pinned but search.exaKey is not set — every search answers \"Search failed (exa): no API key\". Choose auto, or set the key.") { t.Fatalf("the pinned searching row reads:\n%s", screen) } diff --git a/internal/tui3/settingsgate_test.go b/internal/tui3/settingsgate_test.go index 964834ce7c..c7c91a5cfd 100644 --- a/internal/tui3/settingsgate_test.go +++ b/internal/tui3/settingsgate_test.go @@ -62,12 +62,12 @@ func TestTheGateRowReachesTheRunningGateAndTheBadgeOnTheSameKeystroke(t *testing return nil } - // allow → deny → prompt, which is the registry's own order and two presses - // of the one key that answers a cycle row. - drive(t, a, key("enter"), key("enter")) + // Explicit choices reach the live gate only when saved. + chooseSheetValue(t, a, "deny") + chooseSheetValue(t, a, "prompt") if got := config.ToolApprovalModeAt(dir); got != "prompt" { - t.Fatalf("the profile row reads %q after two cycles, want prompt", got) + t.Fatalf("the profile row reads %q after two saved choices, want prompt", got) } if told != 2 { t.Fatalf("the running gate was rebuilt %d times over two changes, want 2", told) @@ -86,7 +86,7 @@ func TestTheGateRowReachesTheRunningGateAndTheBadgeOnTheSameKeystroke(t *testing // AND THE OTHER DIRECTION, which is the dangerous one: the asking goes off // and the badge has to appear on the same keystroke. - drive(t, a, key("enter")) + chooseSheetValue(t, a, "allow") if got := config.ToolApprovalModeAt(dir); got != "allow" { t.Fatalf("the profile row reads %q, want allow", got) } @@ -158,7 +158,7 @@ func TestAGateRowThatCouldNotBeRebuiltNamesTheNextSession(t *testing.T) { a, dir := gateSheet(t, "allow") a.applyApprovals = seam.push - drive(t, a, key("enter"), key("enter")) + chooseSheetValue(t, a, "prompt") if got := config.ToolApprovalModeAt(dir); got != "prompt" { t.Fatalf("the write did not stand: the row reads %q", got) diff --git a/internal/tui3/settingspend_test.go b/internal/tui3/settingspend_test.go index b474c96772..12d4a32ec1 100644 --- a/internal/tui3/settingspend_test.go +++ b/internal/tui3/settingspend_test.go @@ -53,8 +53,7 @@ func TestTheSpendingTabIsTheSevenRowsAndNothingElse(t *testing.T) { a.dayCost, a.dayCosted = 3.42, true a.sheet.today = a.todayReading() a.sheet.build() - want := []string{spendTodayWord, "per day", "per conversation", "per plan", - "per task", "per standing run", "practice"} + want := []string{spendTodayWord, "daily spending limit", "per-chat spending limit", "ask before a plan costs more than", "per task", "per standing run"} if got := spendingRows(a); strings.Join(got, "|") != strings.Join(want, "|") { t.Fatalf("the Spending tab reads\n %v\nwant\n %v", got, want) } @@ -85,8 +84,8 @@ func TestSafetyAndTasksHoldWhatSpendingLetGoAndWorkspaceNamesNoMoney(t *testing. config.KeyBashApprovals: tabSafety, config.KeyGuardian: tabSafety, config.KeyConsentTimeout: tabSafety, - config.KeyBashBackgroundAfter: tabSafety, - config.KeyTaskSettle: tabSafety, + config.KeyBashBackgroundAfter: tabTasks, + config.KeyTaskSettle: tabTasks, config.KeyTaskAutoApprove: tabSafety, config.KeyTaskStart: tabTasks, config.KeyTaskAudit: tabTasks, @@ -97,7 +96,6 @@ func TestSafetyAndTasksHoldWhatSpendingLetGoAndWorkspaceNamesNoMoney(t *testing. config.KeyTaskModel: tabTasks, config.KeyDailyBudget: tabSpending, config.KeyPlanConsent: tabSpending, - config.KeyPracticeBudget: tabSpending, config.KeySpendRail: tabSpending, } for key, tab := range want { @@ -105,18 +103,10 @@ func TestSafetyAndTasksHoldWhatSpendingLetGoAndWorkspaceNamesNoMoney(t *testing. t.Errorf("%s is drawn on %q, want %q", key, where[key], tab) } } - // THE THREE NEW TABS ARE THE REGISTRY'S THREE CATEGORIES, ONE TO ONE. A row - // filed under one and drawn under another is two answers to one question. + // Presentation categories may differ from the resident registry without changing storage. for _, row := range a.sheet.rows { - meta, ok := settingMetaFor(row) - if !ok { - continue - } - if category, mapped := settingTabCategory[meta.tab]; mapped && row.Category != category { - t.Errorf("%s is drawn on %q and filed under %q", row.Key, meta.tab, row.Category) - } - if tab, mapped := tabForCategory(row.Category); mapped && meta.tab != tab { - t.Errorf("%s is filed under %q and drawn on %q", row.Key, row.Category, meta.tab) + if meta, ok := settingMetaFor(row); ok && meta.tab != row.ChatPresentation().Category { + t.Errorf("%s presentation drift", row.Key) } } // AND NOTHING ON WORKSPACE NAMES MONEY. @@ -126,8 +116,7 @@ func TestSafetyAndTasksHoldWhatSpendingLetGoAndWorkspaceNamesNoMoney(t *testing. } } // AND THE BAR READS IN THE DESIGN'S ORDER. - want2 := []string{tabSession, tabContext, tabWorkspace, tabDisplay, - tabSpending, tabSafety, tabTasks, tabTeams, tabProviders, tabConnections} + want2 := []string{tabGeneral, tabProviders, tabMemory, tabTasks, tabTeams, tabSafety, tabSpending, tabConnections, tabPrivacy} if strings.Join(settingTabs, " ") != strings.Join(want2, " ") { t.Fatalf("the tab bar reads %v", settingTabs) } @@ -208,10 +197,9 @@ func mustSpendRow(t *testing.T, a *app, key string) config.Setting { func TestNoLimitIsAWordAndZeroIsNeverDrawn(t *testing.T) { a, _ := spendingSheet(t) rows := map[string]string{ - config.KeyDailyBudget: config.NoLimitWord, - config.KeySpendRail: config.NoLimitWord, - config.KeyPlanConsent: "never asks", - config.KeyPracticeBudget: "practice off", + config.KeyDailyBudget: config.NoLimitWord, + config.KeySpendRail: config.NoLimitWord, + config.KeyPlanConsent: "never asks", } for key, word := range rows { for _, typed := range []string{"none", "no", "off", "unlimited", "∞", "0", "$0"} { @@ -318,10 +306,20 @@ func TestEveryDoorLandsOnTheSameEditor(t *testing.T) { if rail := config.SpendRailUSDAt(dir); rail != 5 { t.Fatalf("/budget conversation 5 wrote %v", rail) } - // A row named with no figure is a question, and the answer is the row. + // Resident-only practice is not offered as a chat setting. + if _, available := budgetRowFor("practice"); available { + t.Fatal("resident-only budget door is offered") + } a.budget("practice") - if item, ok := a.sheet.current(); !ok || item.row.Key != config.KeyPracticeBudget { - t.Fatal("/budget practice lands on the practice row") + refused := false + for _, entry := range a.entries { + refused = refused || strings.Contains(entry.text, "Practice limits are not used by chats") + } + if !refused { + t.Fatal("unsupported practice door gave no explanation") + } + if row, _ := a.registry().Row(config.KeyPracticeBudget); !row.ChatPresentation().Hidden { + t.Fatal("resident practice row is visible") } // A TASK IS NOT A ROW, because there is no per-task rail to write. if _, ok := budgetRowFor("task"); ok { @@ -437,7 +435,7 @@ func TestAtSixtyColumnsEveryLabelSurvivesAndTheReceiptGoesFirst(t *testing.T) { t.Fatalf("a label was cut at sixty columns: %q", line) } } - for _, want := range []string{"per day", "per conversation", "per plan", "per task", "practice"} { + for _, want := range []string{"daily spending limit", "per-chat spending limit", "ask before a plan costs more than", "per task"} { if !sheetHas(a, want) { t.Fatalf("%q did not survive sixty columns", want) } diff --git a/internal/tui3/settingsroles_test.go b/internal/tui3/settingsroles_test.go index e6a8f0f54b..cdb35681bd 100644 --- a/internal/tui3/settingsroles_test.go +++ b/internal/tui3/settingsroles_test.go @@ -80,6 +80,10 @@ func toProviders(t *testing.T, a *app) { for at, title := range settingTabs { if title == tabProviders { a.sheet.tab = at + if a.sheet.advanced == nil { + a.sheet.advanced = map[string]bool{} + } + a.sheet.advanced[tabProviders] = true a.sheet.build() return } diff --git a/internal/tui3/settingssearch_test.go b/internal/tui3/settingssearch_test.go index 29b9c689c1..f84e7c28a8 100644 --- a/internal/tui3/settingssearch_test.go +++ b/internal/tui3/settingssearch_test.go @@ -42,7 +42,7 @@ func TestTheSettingsSearchFindsARowByItsAboutLine(t *testing.T) { a, _ := sheetApp(t) a.openSettings() labels := searchLabels(t, a, "dangerous") - if !screenHas(labels, "ask before running") { + if !screenHas(labels, "tool approvals") { t.Fatalf("\"dangerous\" did not find the approval gate:\n%s", strings.Join(labels, "\n")) } } @@ -73,30 +73,23 @@ func itemAt(items []sheetItem, key string) int { // its key nor its about line carries the word — a person who knows what the // panel does and not what it is called searches the way this test does. func TestTheSettingsSearchFindsARowByItsCurrentValue(t *testing.T) { - a, dir := sheetApp(t) + a, _ := sheetApp(t) a.openSettings() - // This search fixture exercises an explicitly saved prompt posture. - cursorTo(t, a, config.KeyToolApprovalMode) - drive(t, a, key("enter")) // allow -> deny - drive(t, a, key("enter")) // deny -> prompt - if got := config.ToolApprovalModeAt(dir); got != "prompt" { - t.Fatalf("the gate did not start at prompt: %q", got) + cursorTo(t, a, config.KeyToolApprovals) + item, _ := a.sheet.current() + // A tool rule is user content, absent from the label, key, aliases and help. + // Using "prompt" here also matched the new approval explanation fuzzily. + if err := item.row.Apply("grep:deny"); err != nil { + t.Fatal(err) } - // `prompt` is the probe: the gate's own label, key and about line carry no - // `p` at all, so the word cannot reach the row through any of them — not - // even as a scattered subsequence, which is the reach a fuzzy matcher - // always has — and only the value it holds can carry it. - if at := itemAt(searchItems(t, a, "prompt"), config.KeyToolApprovalMode); at < 0 { - t.Fatal("\"prompt\" did not find the gate holding it") + if at := itemAt(searchItems(t, a, "grep:deny"), config.KeyToolApprovals); at < 0 { + t.Fatal("search did not find the saved tool rule") } - // Cycle the gate away from prompt, and the same word loses the row. - cursorTo(t, a, config.KeyToolApprovalMode) - drive(t, a, key("enter")) - if got := config.ToolApprovalModeAt(dir); got != "allow" { - t.Fatalf("the cycle wrote %q, want allow", got) + if err := item.row.Apply(""); err != nil { + t.Fatal(err) } - if at := itemAt(searchItems(t, a, "prompt"), config.KeyToolApprovalMode); at >= 0 { - t.Fatal("\"prompt\" still found the gate after the gate stopped carrying it") + if at := itemAt(searchItems(t, a, "grep:deny"), config.KeyToolApprovals); at >= 0 { + t.Fatal("search retained a value that was cleared") } } @@ -130,11 +123,10 @@ func TestTheSearchCarriesTheMatchedLettersInBold(t *testing.T) { a.openSettings() // This search fixture exercises an explicitly saved prompt posture. cursorTo(t, a, config.KeyToolApprovalMode) - drive(t, a, key("enter")) // allow -> deny - drive(t, a, key("enter")) // deny -> prompt - typeQuery(t, a, "ask") + chooseSheetValue(t, a, "prompt") + typeQuery(t, a, "tool") // The approval row, kept by the search, and where the query landed on it: - // the label's own head, because "ask" is the word the row's name begins + // the label's own head, because "tool" is the word the row's name begins // with — and not its middle, and not a whole word either. at := -1 for i, item := range a.sheet.items { @@ -144,7 +136,7 @@ func TestTheSearchCarriesTheMatchedLettersInBold(t *testing.T) { } } if at < 0 { - t.Fatalf("\"ask\" did not keep the approval row:\n%s", strings.Join(sheetLabels(a), "\n")) + t.Fatalf("\"tool\" did not keep the approval row:\n%s", strings.Join(sheetLabels(a), "\n")) } // Walk the cursor off the row: the cursor's row is bold whole already, and // this test is about the resting rows a person scans down. @@ -155,15 +147,15 @@ func TestTheSearchCarriesTheMatchedLettersInBold(t *testing.T) { t.Fatal("the cursor never left the approval row") } hit := a.sheet.itemHit(a.sheet.items[at]) - if len(hit) != 3 || hit[0] != 0 || hit[1] != 1 || hit[2] != 2 { - t.Fatalf("the approval row's label hit is %v; want its first three bytes, 0 1 2", hit) + if len(hit) != 4 || hit[0] != 0 || hit[1] != 1 || hit[2] != 2 || hit[3] != 3 { + t.Fatalf("the approval row's label hit is %v; want its first four bytes, 0 1 2 3", hit) } lines, _ := a.sheet.listLines(a.width, a.height, a.pal, -1) // THE EMPHASIS IS EXACTLY THE MATCHED BYTES: bold over the row's own dim - // for "ask", plain dim after it — the whole label spelled out, so a bold + // for "tool", plain dim after it — the whole label spelled out, so a bold // run that bled past the match, or one that skipped a matched byte, does // not contain this string. - want := a.pal.bold(a.pal.dim("ask")) + a.pal.dim(" before running") + want := a.pal.bold(a.pal.dim("tool")) + a.pal.dim(" approvals") found := false for _, line := range lines { if strings.Contains(line, want) { @@ -176,7 +168,7 @@ func TestTheSearchCarriesTheMatchedLettersInBold(t *testing.T) { } // AND THE MATCH IS ON THE FIELD IT WON AND NOWHERE ELSE: "prompt" is // the approval row's own VALUE — the word the gate carries — and a query - // of both words keeps the row with "ask" on its label and nothing else: + // of both words keeps the row with "tool" on its label and nothing else: // the span names exactly the bytes of the term that won the label, and a // term that won the value contributes no emphasis to a field it did not. typeQuery(t, a, " prompt") @@ -188,10 +180,10 @@ func TestTheSearchCarriesTheMatchedLettersInBold(t *testing.T) { } } if at < 0 { - t.Fatalf("\"ask prompt\" did not keep the approval row:\n%s", strings.Join(sheetLabels(a), "\n")) + t.Fatalf("\"tool prompt\" did not keep the approval row:\n%s", strings.Join(sheetLabels(a), "\n")) } - if hit := a.sheet.itemHit(a.sheet.items[at]); !reflect.DeepEqual(hit, []int{0, 1, 2}) { - t.Fatalf("\"ask prompt\" carried %v on the approval row's label; want exactly \"ask\"'s three bytes — the value's word stays on the value", hit) + if hit := a.sheet.itemHit(a.sheet.items[at]); !reflect.DeepEqual(hit, []int{0, 1, 2, 3}) { + t.Fatalf("\"tool prompt\" carried %v on the approval row's label; want exactly \"tool\"'s four bytes — the value's word stays on the value", hit) } } @@ -220,17 +212,29 @@ func TestSpaceWhileSearchingTypesAndDoesNotPressTheRow(t *testing.T) { a.openSettings() // This search fixture exercises an explicitly saved prompt posture. cursorTo(t, a, config.KeyToolApprovalMode) - drive(t, a, key("enter")) // allow -> deny - drive(t, a, key("enter")) // deny -> prompt + chooseSheetValue(t, a, "prompt") cursorTo(t, a, config.KeyToolApprovalMode) // The space with no search open is the panel's own gesture: it activates. drive(t, a, key(" ")) + if a.sheet.choice == nil { + t.Fatal("space out of a search did not open the choices") + } + if got := config.ToolApprovalModeAt(dir); got != "prompt" { + t.Fatalf("opening choices changed the gate: %q", got) + } + drive(t, a, key("home")) + for _, value := range a.sheet.choice.item.row.Choices { + if value == "allow" { + break + } + drive(t, a, key("down")) + } + drive(t, a, key("enter")) if got := config.ToolApprovalModeAt(dir); got != "allow" { - t.Fatalf("space out of a search did not activate the row: gate still %q", got) + t.Fatalf("saving the selected choice did not change the gate: %q", got) } // Put the gate back, start a search, and the same key is the box's. - drive(t, a, key("enter")) // allow -> deny - drive(t, a, key("enter")) // deny -> prompt + chooseSheetValue(t, a, "prompt") if got := config.ToolApprovalModeAt(dir); got != "prompt" { t.Fatalf("the gate did not come back around to prompt: %q", got) } @@ -265,8 +269,8 @@ func TestTheSettingsSearchRanksItsBestAnswerFirst(t *testing.T) { if countdown < 0 || gate < 0 { t.Fatalf("\"approval\" found only %d of its two rows", countdown+gate+2) } - if countdown > gate { - t.Fatalf("the key hit (row %d) ranked above the label prefix (row %d)", gate, countdown) + if gate > countdown { + t.Fatalf("the direct tool approvals label (row %d) ranked below timer row %d", gate, countdown) } } diff --git a/internal/tui3/settingunit_test.go b/internal/tui3/settingunit_test.go index c6cad1e687..e52058e6f9 100644 --- a/internal/tui3/settingunit_test.go +++ b/internal/tui3/settingunit_test.go @@ -22,6 +22,10 @@ func sheetRowsDrawn(t *testing.T, a *app, width int) []string { a.sheet.build() }() for tab := range settingTabs { + if a.sheet.advanced == nil { + a.sheet.advanced = map[string]bool{} + } + a.sheet.advanced[settingTabs[tab]] = true a.sheet.tab = tab a.sheet.cursor, a.sheet.top = 0, 0 a.sheet.build() @@ -51,18 +55,17 @@ func TestEverySettingRowDrawsWhatItsNumberMeans(t *testing.T) { drawn := sheetRowsDrawn(t, a, 120) for _, want := range []string{ - "ssh reuse", "300s", - "ssh heartbeat", "3s", - "approval countdown", "10s", - "background after", "30s", - "task countdown", "15s", - "compact at", "60%", - "answer room", "65536 tok", - "working set", "160000 tok", - "context reuse", "250%", - "memory floor", "1536 MB", - "busy machine", "1.5 per core", - "tenure after", "3 clean firings", + "SSH connection reuse", "300s", + "SSH keepalive interval", "3s", + "pause approval timer after", "10s", + "background shell commands after", "30s", + "start proposed tasks after", "15s", + "compact context at", "60%", + "tokens reserved for replies", "65536 tok", + "working context limit", "160000 tok", + "context reuse limit", "250%", + "minimum available RAM", "1536 MB", + "maximum load per CPU core", "1.5 per core", } { found := false for _, row := range drawn { @@ -87,8 +90,8 @@ func TestEverySettingRowDrawsWhatItsNumberMeans(t *testing.T) { continue } key := strings.Join(fields[:len(fields)-1], " ") - if strings.Contains(key, "rounds") || strings.Contains(key, "tasks") || - strings.Contains(key, "heartbeats") || strings.HasPrefix(row, "$") { + if strings.Contains(key, "rounds") || strings.Contains(key, "attempts") || strings.Contains(key, "tasks") || + strings.Contains(key, "keepalives") || strings.HasPrefix(row, "$") { // The label names what is counted, which is the other way a row may // answer ([config.UnitInLabel]). continue @@ -121,17 +124,15 @@ func bareFigure(s string) bool { // stored moves: the keys are untouched and only the tab a row is drawn under // changed. func TestTheSshRowsAreOnTheTabAboutReachingAnotherMachine(t *testing.T) { - for _, key := range []string{ - config.KeySSHControlPersist, config.KeySSHServerAlive, - config.KeySSHServerMisses, config.KeySSHIPQoS, - } { - meta, ok := settingUI[key] + registry := config.NewSettings(config.SettingsOptions{ProfileDir: t.TempDir()}) + for _, key := range []string{config.KeySSHControlPersist, config.KeySSHServerAlive, config.KeySSHServerMisses, config.KeySSHIPQoS} { + row, ok := registry.Row(key) if !ok { - t.Fatalf("row %q has no place on the panel at all", key) + t.Fatalf("missing registry row %s", key) } - if meta.tab != tabWorkspace { - t.Fatalf("row %q is drawn under %q\n drawn: %s tab · %s\n want: %s tab · %s", - key, meta.tab, meta.tab, meta.label, tabWorkspace, meta.label) + meta, ok := settingMetaFor(row) + if !ok || meta.tab != tabConnections || !row.ChatPresentation().Advanced { + t.Fatalf("%s must be in Connections Advanced", key) } } } @@ -296,6 +297,10 @@ func sheetRowDrawn(t *testing.T, a *app, label string, width int) (string, strin a.sheet.build() }() for tab := range settingTabs { + if a.sheet.advanced == nil { + a.sheet.advanced = map[string]bool{} + } + a.sheet.advanced[settingTabs[tab]] = true a.sheet.tab = tab a.sheet.cursor, a.sheet.top = 0, 0 a.sheet.build() @@ -367,12 +372,12 @@ func TestASettingsValueSitsInAColumnAndNeverButtsItsLabel(t *testing.T) { // A HUNDRED AND SIXTY: the same function, the other end. The tail stops at // the measure and the rest of the frame is left empty. - wide, _ := sheetRowDrawn(t, a, "ssh reuse", 160) + wide, _ := sheetRowDrawn(t, a, "SSH connection reuse", 160) if got := ansi.StringWidth(wide); got > 102 { t.Fatalf("at 160 cells the row is %d cells wide and reads\n %q\n— the value is right-aligned to the frame "+ "rather than to the %d-cell measure", got, wide, overlayMeasure) } - if !strings.Contains(wide, "ssh reuse") || !strings.Contains(wide, "300s") { + if !strings.Contains(wide, "SSH connection reuse") || !strings.Contains(wide, "300s") { t.Fatalf("the 160-cell row is no longer the pair this test is about: %q", wide) } diff --git a/internal/tui3/switcher_test.go b/internal/tui3/switcher_test.go index 95c74e562c..7c02b8a2b6 100644 --- a/internal/tui3/switcher_test.go +++ b/internal/tui3/switcher_test.go @@ -177,7 +177,7 @@ func TestTheSwitcherSinceYouLeftLedgerDrawsOnlyRecordedDoors(t *testing.T) { for _, row := range r.ledger { doors[row.place] = true } - for _, door := range []string{"standing", "memory", "sessions"} { + for _, door := range []string{"standing", "memory", "activity"} { if !doors[door] { t.Fatalf("%s was not a ledger door", door) } diff --git a/internal/tui3/tasksplace.go b/internal/tui3/tasksplace.go index 65dc363101..9af9544613 100644 --- a/internal/tui3/tasksplace.go +++ b/internal/tui3/tasksplace.go @@ -1745,7 +1745,7 @@ func (r tasksReading) head(width int, edge bool) string { // tasksHeadWord names the place, in the word the switcher's own tab spells // (pages.go's [pageTasks]). One name for one place. -const sessionsWord = "sessions" +const sessionsWord = "activity" const tasksHeadWord = sessionsWord func tasksWindowStart(win session.UsageWindow) string { diff --git a/internal/tui3/taskview_test.go b/internal/tui3/taskview_test.go index 5428eb0a67..002ff9b843 100644 --- a/internal/tui3/taskview_test.go +++ b/internal/tui3/taskview_test.go @@ -85,7 +85,7 @@ func TestTheTaskPageOpensOnItsKeyAndTakesTheWholeFrame(t *testing.T) { // title row, `history … esc close`, across its head; the router draws the // seven places above the rule and the shared hint line says how to leave, so // a title here would be the frame naming itself twice (pages.go). - if !strings.Contains(plain(frame), pageTasks.word()) { + if !strings.Contains(plain(frame), navLabel(pageTasks)) { t.Fatalf("the frame is not the tasks place:\n%s", plain(frame)) } // The conversation is not drawn under it, and neither is the box a person @@ -231,7 +231,7 @@ func TestTheTaskPageCommandIsHistoryAndNothingSpellsItTasks(t *testing.T) { // the manual both call it. The COMMAND keeps its own older word — /history // still opens this, and [taskSheetWord] is still what that command is called // — which is why the two are checked apart. - if text := taskSheetText(a); !strings.Contains(text, pageTasks.word()) { + if text := taskSheetText(a); !strings.Contains(text, navLabel(pageTasks)) { t.Fatalf("the place does not say what it is:\n%s", text) } } diff --git a/internal/tui3/teamcreate.go b/internal/tui3/teamcreate.go index e02a4d7748..83bb61492e 100644 --- a/internal/tui3/teamcreate.go +++ b/internal/tui3/teamcreate.go @@ -262,9 +262,9 @@ func (a *app) teamCreateOver(frame string) string { if inner < 28 || height < 14 { return frame } - title := "New team" + title := "New AI team" if s.parent != "" { - title = "New team in " + a.teamNameOf(s.parent) + title = "New AI team in " + a.teamNameOf(s.parent) } text := func(value string, field int) string { if s.field == field { diff --git a/internal/tui3/teammanager.go b/internal/tui3/teammanager.go index c2671f1164..a61a33bf4d 100644 --- a/internal/tui3/teammanager.go +++ b/internal/tui3/teammanager.go @@ -48,7 +48,7 @@ const ( // and `● harbor ▾ ◆ harbor` read as two harbors. const ( teamGlobalManagerSlotWord = "+ Global manager" - teamManagerSlotWord = "+ Manager" + teamManagerSlotWord = "+ AI manager" teamManagerWord = "Manager" ) diff --git a/internal/tui3/teammembership.go b/internal/tui3/teammembership.go index 888f7167f8..1db24028a4 100644 --- a/internal/tui3/teammembership.go +++ b/internal/tui3/teammembership.go @@ -454,9 +454,9 @@ func (a *app) teamMembershipOver(frame string) string { s.rect = wallRect{} return frame } - title := "Add member to " + t.Name + title := "Add chat to " + t.Name if s.choosingManager { - title = "Choose manager for " + t.Name + title = "Choose AI manager for " + t.Name } var lines []wallCardLine enterAction := "choose" diff --git a/internal/tui3/teammenu_test.go b/internal/tui3/teammenu_test.go index ea54db74a3..bf8d1f0a0b 100644 --- a/internal/tui3/teammenu_test.go +++ b/internal/tui3/teammenu_test.go @@ -188,7 +188,7 @@ func TestTeamMenuOffersOnlyOverlayChoices(t *testing.T) { a, _, _ := menuApp(t) a.openTeamMenu() frame, _ := menuFrame(t, a) - for _, word := range []string{"None", "Add this conversation", "Remove this conversation", "New team", "Team settings", "Make manager", "Closed"} { + for _, word := range []string{"None", "Add this conversation", "Remove this conversation", "New AI team", "Team settings", "Make manager", "Closed"} { if strings.Contains(frame, word) { t.Fatalf("management action %q remains in overlay picker", word) } diff --git a/internal/tui3/teamnest_test.go b/internal/tui3/teamnest_test.go index 051d02afcf..53a7cb653f 100644 --- a/internal/tui3/teamnest_test.go +++ b/internal/tui3/teamnest_test.go @@ -376,7 +376,7 @@ func TestNewTeamInTheChosenTeam(t *testing.T) { if a.wall.on || !a.tcreate.on || a.tcreate.parent != harbor { t.Fatalf("the new-team card is not for a team in harbor: on %v naming %v parent %q", a.wall.on, a.wall.naming, a.wall.nameParent) } - if !strings.Contains(teamsFrameText(a), "New team in harbor") { + if !strings.Contains(teamsFrameText(a), "New AI team in harbor") { t.Fatalf("the card does not say where the team goes:\n%s", teamsFrameText(a)) } a.tcreate.name.setText("slip") @@ -475,7 +475,7 @@ func TestTheTeamHeaderIsOneLineAndDropsInOrder(t *testing.T) { t.Fatalf("the sidebar lost the selected team at %d:\n%s", width, text) } if width >= 110 { - add := strings.Index(line, "+ Add member") + add := strings.Index(line, "+ Add chat") sub := strings.Index(line, "+ Add subteam") if add < 0 || sub <= add || !strings.Contains(line, "Disband") { t.Fatalf("the header lost its controls at %d: %q", width, line) diff --git a/internal/tui3/teamoverlay_test.go b/internal/tui3/teamoverlay_test.go index b178f8feca..2fd43308c7 100644 --- a/internal/tui3/teamoverlay_test.go +++ b/internal/tui3/teamoverlay_test.go @@ -142,7 +142,7 @@ func TestTeamMembershipControlsRemainVisibleAndDoNotOverlapConversationLinks(t * for _, width := range []int{40, 72, 100, 160} { a.width, a.height = width, 42 text := teamsFrameText(a) - if !strings.Contains(text, "+ Add member") || strings.Contains(text, "Actions") || !strings.Contains(text, "+ Add subteam") || !strings.Contains(text, "Choose manager") { + if !strings.Contains(text, "+ Add chat") || strings.Contains(text, "Actions") || !strings.Contains(text, "+ Add subteam") || !strings.Contains(text, "Choose AI manager") { t.Fatalf("membership controls missing at %d:\n%s", width, text) } for _, action := range a.tp.targets { diff --git a/internal/tui3/teamsall_test.go b/internal/tui3/teamsall_test.go index 1dc9162d15..9fa105f78b 100644 --- a/internal/tui3/teamsall_test.go +++ b/internal/tui3/teamsall_test.go @@ -84,7 +84,7 @@ func TestTeamsAllOverviewKeepsGlobalManagerAndDecisions(t *testing.T) { a.tp.previews[root.Manager] = teamsPreview{text: "Current global update"} a.tp.sel = teamsAllRow text := teamsFrameText(a) - for _, want := range []string{"@global", "Settings", "+ Add member", "Which layout first?", "Global decision", "Subteams"} { + for _, want := range []string{"@global", "Settings", "+ Add chat", "Which layout first?", "Global decision", "Subteams"} { if !strings.Contains(text, want) { t.Fatalf("missing %s:\n%s", want, text) } diff --git a/internal/tui3/teamsglobal.go b/internal/tui3/teamsglobal.go index 79b1521cc0..d0c27d3070 100644 --- a/internal/tui3/teamsglobal.go +++ b/internal/tui3/teamsglobal.go @@ -94,7 +94,7 @@ func (a *app) teamsGlobalManagerCard(d *teamsDraw, width, y int) []string { action(teamGlobalManagerSlotWord, teamsActRootManager, "", "", "Create the optional global manager conversation") } if hasManager { - action("+ Add member", teamsActAddMember, root.ID, "", "Add a conversation to the global manager team") + action("+ Add chat", teamsActAddMember, root.ID, "", "Add a conversation to the global manager team") action("Settings", teamsActSettings, root.ID, "", "Global manager settings and spending controls") } line, x := "", 0 diff --git a/internal/tui3/teamsglobal_test.go b/internal/tui3/teamsglobal_test.go index f4a1885ee0..25b39d2c14 100644 --- a/internal/tui3/teamsglobal_test.go +++ b/internal/tui3/teamsglobal_test.go @@ -66,12 +66,12 @@ func TestTeamsGlobalManagerCardLinksPreviewAndDeletesOnlyItsConversation(t *test a.tp.previews = map[string]teamsPreview{key: {text: "Actual saved update from the global manager"}} d := &teamsDraw{a: a} text := plain(strings.Join(a.teamsGlobalManagerCard(d, 90, 3), "\n")) - for _, word := range []string{"Global manager", "@global", "Actual saved update", "+ Add member", "Settings"} { + for _, word := range []string{"Global manager", "@global", "Actual saved update", "+ Add chat", "Settings"} { if !strings.Contains(text, word) { t.Fatalf("missing %s: %s", word, text) } } - for _, word := range []string{teamGlobalManagerSlotWord, "Choose manager"} { + for _, word := range []string{teamGlobalManagerSlotWord, "Choose AI manager"} { if strings.Contains(text, word) { t.Fatalf("populated global card has %s", word) } @@ -172,7 +172,7 @@ func TestTeamsGlobalManagerDeletionRefreshesWithoutOtherHeldMembers(t *testing.T a.tp.sel = teamsAllRow a.page = pageTeams text := plain(strings.Join(a.teamsGlobalManagerCard(&teamsDraw{a: a}, 100, 0), "\n")) - if strings.Contains(text, "+ Add member") || strings.Contains(text, "Settings") || strings.Contains(text, "Choose manager") { + if strings.Contains(text, "+ Add chat") || strings.Contains(text, "Settings") || strings.Contains(text, "Choose AI manager") { t.Fatal("deleted manager card retained controls") } if !strings.Contains(text, teamGlobalManagerSlotWord) || strings.Contains(text, "Conversation unavailable") { @@ -339,7 +339,7 @@ func TestEmptyTeamsPageOffersGlobalCreationOnlyInItsCard(t *testing.T) { } label := "Organize" if target.act == teamsActNewTeam { - label = "New team" + label = "New AI team" } if !strings.Contains(ansi.Cut(rows[target.y], target.x0, target.x1), label) { t.Fatalf("empty-page %s hit misses visible label: %+v", label, target) diff --git a/internal/tui3/teamsoverview.go b/internal/tui3/teamsoverview.go index fc5b0c5435..6b1d6c4780 100644 --- a/internal/tui3/teamsoverview.go +++ b/internal/tui3/teamsoverview.go @@ -164,7 +164,7 @@ func (a *app) teamsOverviewHeader(d *teamsDraw, t team, width, y int) string { } left = fit(left, leftRoom) if !t.Closed() { - for _, c := range []control{{"+ Add member", teamsActAddMember}, {"+ Add subteam", teamsActAddSubteam}, {"Choose manager", teamsActChooseManager}} { + for _, c := range []control{{"+ Add chat", teamsActAddMember}, {"+ Add subteam", teamsActAddSubteam}, {"Choose AI manager", teamsActChooseManager}} { if t.Root && c.act == teamsActAddSubteam { continue } diff --git a/internal/tui3/teamspage.go b/internal/tui3/teamspage.go index 86ef1519dd..9a1eb28e7e 100644 --- a/internal/tui3/teamspage.go +++ b/internal/tui3/teamspage.go @@ -34,10 +34,10 @@ const teamsInboxWhole = 3 // The page's words, quoted in the manual exactly as spelled here. const ( - teamsExplainWord = "A team is a set of conversations you run together; give it a manager and you talk to the manager, which hands out the work and asks you only what it cannot decide." - teamsNoManagerWord = "a manager takes your messages to the team and asks you only what it cannot decide" + teamsExplainWord = "An AI team groups chats around ongoing work. Add an optional AI manager to coordinate them." + teamsNoManagerWord = "an optional AI manager coordinates these chats and brings questions to you" teamsOrganizeWord = "Organize my conversations" - teamsNewTeamWord = "New team" + teamsNewTeamWord = "New AI team" teamsHostedWord = "the inbox and the spend are not available over this connection" ) diff --git a/internal/tui3/teamspage_test.go b/internal/tui3/teamspage_test.go index 7e286856dd..4dc3ac60ac 100644 --- a/internal/tui3/teamspage_test.go +++ b/internal/tui3/teamspage_test.go @@ -101,7 +101,7 @@ func TestTeamsIsTheSecondPlaceOnTheBarTheDigitsAndTheCommand(t *testing.T) { } a := placeApp(t) bar := navPlaces(a, 120, false) - if !placeWordsInOrder(bar, "home", "teams", "chats", "sessions") { + if !placeWordsInOrder(bar, "Home", "Chats", "AI teams", "Activity") { t.Fatalf("the bar does not put teams after home: %q", bar) } drive(t, a, key("alt+2")) @@ -122,7 +122,7 @@ func TestTeamsWithNoTeamsSaysWhatTheyAreAndOffersTwoWays(t *testing.T) { a.width, a.height = 110, 24 drive(t, a, key("alt+2")) text := teamsFrameText(a) - for _, want := range []string{"A team is a set of conversations", teamsOrganizeWord, teamsNewTeamWord} { + for _, want := range []string{"An AI team groups chats", teamsOrganizeWord, teamsNewTeamWord} { if !strings.Contains(text, want) { t.Fatalf("the empty page lost %q:\n%s", want, text) } diff --git a/internal/tui3/teamspagedraw.go b/internal/tui3/teamspagedraw.go index 255f8f6cd6..bbd6f7141b 100644 --- a/internal/tui3/teamspagedraw.go +++ b/internal/tui3/teamspagedraw.go @@ -362,7 +362,7 @@ func (a *app) teamsTop(d *teamsDraw, width int) []string { for _, control := range []struct { word string act teamsAct - }{{"+ Add member", teamsActAddMember}, {"+ Add subteam", teamsActAddSubteam}, {"Choose manager", teamsActChooseManager}} { + }{{"+ Add chat", teamsActAddMember}, {"+ Add subteam", teamsActAddSubteam}, {"Choose AI manager", teamsActChooseManager}} { if t.Root && control.act == teamsActAddSubteam { continue } diff --git a/internal/tui3/teamspreview_test.go b/internal/tui3/teamspreview_test.go index 52d07960d7..c60d8d1c41 100644 --- a/internal/tui3/teamspreview_test.go +++ b/internal/tui3/teamspreview_test.go @@ -142,7 +142,7 @@ func TestTeamsMemberAndGlobalPreviewsUseTheSameLatestExchange(t *testing.T) { d = &teamsDraw{a: a} rows := a.teamsGlobalManagerCard(d, 120, 3) text = plain(strings.Join(rows, "\n")) - for _, want := range []string{a.pal.youGlyph() + "Global prompt", "Global reply begins.", "? Review global work", "+ Add member", "Settings"} { + for _, want := range []string{a.pal.youGlyph() + "Global prompt", "Global reply begins.", "? Review global work", "+ Add chat", "Settings"} { if !strings.Contains(text, want) { t.Fatalf("global missing %q: %s", want, text) } diff --git a/internal/tui3/title_test.go b/internal/tui3/title_test.go index cd5852ffdf..e89d8e79aa 100644 --- a/internal/tui3/title_test.go +++ b/internal/tui3/title_test.go @@ -70,8 +70,8 @@ func TestTheTerminalTitleSaysWhereYouAre(t *testing.T) { // here is a row this test names rather than one it silently skips. func TestEveryPlaceTitlesTheTabWithItsWord(t *testing.T) { want := map[page]string{ - pageTasks: "sessions · codeaf", - pageTeams: "teams · codeaf", + pageTasks: "activity · codeaf", + pageTeams: "AI teams · codeaf", pageStanding: "standing · codeaf", pageMemory: "memory · codeaf", pageSpend: "spend · codeaf", diff --git a/internal/tui3/topnav.go b/internal/tui3/topnav.go index 1584169f13..366fef49c8 100644 --- a/internal/tui3/topnav.go +++ b/internal/tui3/topnav.go @@ -86,6 +86,15 @@ const ( navTailGap = 2 ) +// navLabel changes presentation only; page words remain stable navigation identities. +func navLabel(id page) string { + word := id.word() + if word == "" { + return word + } + return strings.ToUpper(word[:1]) + word[1:] +} + // navMoreWord is the fold's own word, `more ▾`, with the caret every menu on // this surface wears ([app.tabTeamWord]). func (a *app) navMoreWord(pal palette) string { @@ -93,7 +102,7 @@ func (a *app) navMoreWord(pal palette) string { if pal.ascii { caret = "v" } - return "more " + caret + return "More " + caret } // navLit is the place the nav lights: the one a person is standing in, and @@ -500,9 +509,9 @@ func (a *app) headHint() string { case a.navMore.on: return navMoreFootWords case placeFor(a.tabHover) != nil: - return a.chords.say(placeChord(a.tabHover)+" "+a.tabHover.word()) + hintSegment + placeFor(a.tabHover).about() + return a.chords.say(placeChord(a.tabHover)+" "+navLabel(a.tabHover)) + hintSegment + placeFor(a.tabHover).about() case a.navMore.hot: - return "more · the places this row has no room for" + return "More · the places this row has no room for" case a.hot.kind == hoverTab: // THE STRIP'S DOORS SAY WHAT THEY SAY IN A CONVERSATION, in the one // place those sentences are written (walldock.go's diff --git a/internal/tui3/topnav_test.go b/internal/tui3/topnav_test.go index ec12b2ebd7..b375e8d434 100644 --- a/internal/tui3/topnav_test.go +++ b/internal/tui3/topnav_test.go @@ -80,7 +80,7 @@ func TestRowZeroIsTheSameOnEveryPage(t *testing.T) { if a.tabRow != navRow || a.headHeight() != placeHeadRows { t.Fatalf("at %d %s drew its nav on row %d and a %d-row head", width, to.word(), a.tabRow, a.headHeight()) } - if place[navRow] != chat[navRow] || place[1] != chat[1] { + if (width >= 80 && place[navRow] != chat[navRow]) || place[1] != chat[1] { t.Fatalf("at %d %s moved the nav or the air row under it:\nchat %q\nplace %q", width, to.word(), chat[navRow], place[navRow]) } if !strings.HasPrefix(place[placeHeadRows-2], "─") || strings.TrimSpace(place[placeHeadRows-1]) != "" || strings.Contains(place[placeHeadRows-2], "harbor") { @@ -93,7 +93,7 @@ func TestRowZeroIsTheSameOnEveryPage(t *testing.T) { // is where the place drew it, wherever both drew one. for _, c := range chatNav { for _, p := range a.tabs { - if c.id == p.id && c != p { + if width >= 80 && c.id == p.id && c != p { t.Fatalf("at %d %q moved from %+v on the chat to %+v on %s", width, c.id.word(), c, p, to.word()) } } @@ -112,7 +112,7 @@ func TestRowZeroIsTheSameOnEveryPage(t *testing.T) { func TestTheNavLightsWhereYouStand(t *testing.T) { a := navChat(t) a.width = 160 - lit := func(id page) string { return a.pal.bold(a.pal.accent(tabPad + id.word() + tabPad)) } + lit := func(id page) string { return a.pal.bold(a.pal.accent(tabPad + navLabel(id) + tabPad)) } line := a.navLine(a.width, a.pal) if !strings.Contains(line, lit(pageChats)) { t.Fatalf("inside a conversation `chats` is not lit: %q", line) @@ -124,7 +124,7 @@ func TestTheNavLightsWhereYouStand(t *testing.T) { if got := strings.Contains(line, lit(id)); got != (id == to) { t.Fatalf("standing in %s, %s is lit %v", to.word(), id.word(), got) } - if id != to && !strings.Contains(line, a.pal.muted(tabPad+id.word()+tabPad)) { + if id != to && !strings.Contains(line, a.pal.muted(tabPad+navLabel(id)+tabPad)) { t.Fatalf("standing in %s, %s is not muted", to.word(), id.word()) } } @@ -133,7 +133,7 @@ func TestTheNavLightsWhereYouStand(t *testing.T) { // the same cells. a.pal = newPalette(tokens.NoColor, false) walkTo(t, a, pageSpend) - if line := plain(a.navLine(a.width, a.pal)); !strings.Contains(line, "[spend]") { + if line := plain(a.navLine(a.width, a.pal)); !strings.Contains(line, "[Spend]") { t.Fatalf("a plain terminal cannot tell which place is lit: %q", line) } } @@ -276,7 +276,7 @@ func TestTheMoreMenuOpensTheFoldedPlaces(t *testing.T) { } text := plain(frame(a)) for _, id := range folded { - if !strings.Contains(text, id.word()) || !strings.Contains(text, placeChord(id)) { + if !strings.Contains(text, navLabel(id)) || !strings.Contains(text, placeChord(id)) { t.Fatalf("the menu does not offer %q with its key:\n%s", id.word(), text) } } @@ -382,7 +382,7 @@ func TestAResizeDropsTheMoreHint(t *testing.T) { t.Fatal("at 48 the nav drew no more") } a.navHover(a.navMore.span.from, navRow) - if hint := a.footHint(48); !strings.Contains(hint, "more · the places this row has no room for") { + if hint := a.footHint(48); !strings.Contains(hint, "More · the places this row has no room for") { t.Fatalf("hovering more, the hint reads %q", hint) } for _, span := range a.tabs { @@ -399,7 +399,7 @@ func TestAResizeDropsTheMoreHint(t *testing.T) { if a.navMore.span.pressable() { t.Fatal("at 110 more is still a door") } - if hint := a.footHint(a.width); strings.Contains(hint, "more") { + if hint := a.footHint(a.width); strings.Contains(hint, "More") { t.Fatalf("after the resize the hint still says more: %q", hint) } if a.tabHover != pageNone || a.navMore.hot { @@ -414,7 +414,7 @@ func TestTheNavsFoldIsAWordDoorNotACount(t *testing.T) { if strings.Contains(bar, tokens.GlyphCollapsed) || strings.Contains(bar, "+") { t.Fatalf("at %d the nav's fold is a count: %q", width, bar) } - if a.navMore.span.pressable() != strings.HasSuffix(bar, "more ▾") { + if a.navMore.span.pressable() != strings.HasSuffix(bar, "More ▾") { t.Fatalf("at %d the fold's word and its door disagree: %q", width, bar) } } @@ -443,10 +443,10 @@ func TestTheCountThatWantsYouNeverLeavesTheNav(t *testing.T) { a.width = 80 a.navMemo = navMemo{} row := plain(a.navLine(80, a.pal)) - if !placeWordsInOrder(row, "home", "teams", "chats", "sessions", "spend", "settings") || a.navMore.span.pressable() { + if !placeWordsInOrder(row, "Home", "Chats", "AI teams", "Activity", "Memory", "More") || !a.navMore.span.pressable() { t.Fatalf("at 80 the short count cost a place: %q", row) } - if !strings.HasSuffix(strings.TrimRight(row, " "), "2 ? · $1.20 / "+railFigure(20)) { + if !strings.HasSuffix(strings.TrimRight(row, " "), "2 ? · $1.20") { t.Fatalf("at 80 the pulse is not the short count and the allowance: %q", row) } // AND NOTHING WANTING YOU DRAWS NOTHING: no `0 ?`.