Refine user-facing copy

This commit is contained in:
murashit 2026-05-23 08:56:19 +09:00
parent 3a80b56c67
commit 87677be821
16 changed files with 126 additions and 129 deletions

View file

@ -2,7 +2,7 @@
Codex Panel is a desktop-only Obsidian plugin that brings Codex into the right sidebar. It starts `codex app-server` locally, talks to it over stdio, and uses the current vault root as the Codex working directory.
Codex remains the source of truth for model selection, sandboxing, approvals, MCP, hooks, network access, and other runtime policy. Codex Panel provides the Obsidian side-panel UI for that app-server session.
Codex remains the source of truth for models, reasoning effort, sandbox modes, approval policies, MCP servers, hooks, network access, and other runtime policy. Codex Panel provides the Obsidian side-panel UI for that Codex App Server session.
## Requirements
@ -16,68 +16,68 @@ Codex Panel does not support Obsidian mobile.
Install Codex Panel from Obsidian's Community plugins browser:
1. Open `Settings`.
2. Go to `Community plugins`.
3. Select `Browse`.
4. Search for `Codex Panel`.
1. Open **Settings**.
2. Go to **Community plugins**.
3. Select **Browse**.
4. Search for **Codex Panel**.
5. Install and enable the plugin.
You can also open the plugin page directly: <https://community.obsidian.md/plugins/codex-panel>.
## How It Works
## How it works
The current vault root is the app-server working directory. For project vaults, Codex sees the project next to your notes. For plain note vaults, the vault itself is the workspace.
The current vault root is the Codex working directory. For project vaults, Codex sees the project next to your notes. For plain note vaults, the vault itself is the workspace.
Each open panel has its own app-server connection, active thread, pending requests, and composer draft. Thread history, archived threads, hooks, and runtime configuration still come from Codex for the same vault root.
Each open panel has its own Codex App Server connection, active thread, pending requests, and composer draft. Thread history, archived threads, hooks, and runtime configuration still come from Codex for the same vault root.
## Opening the Panel
## Open the panel
Open the command palette and run `Codex Panel: Open panel`, or use the ribbon icon. If Obsidian cannot find `codex`, set `Settings` > `Codex Panel` > `Codex executable` to an absolute path, such as `/opt/homebrew/bin/codex`.
Open the command palette and run **Codex Panel: Open panel**, or select the ribbon icon. If Obsidian cannot find `codex`, set **Settings → Codex Panel → Codex executable** to an absolute path, such as `/opt/homebrew/bin/codex`.
Use `Codex Panel: New chat` to start a fresh thread. Use `Codex Panel: Open new panel` when you want multiple Codex threads side by side.
Use **Codex Panel: New chat** to start a fresh thread. Use **Codex Panel: Open new panel** when you want multiple Codex threads side by side.
## Supported Codex Workflows
## Supported Codex workflows
Codex Panel supports the app-server-backed Codex workflows that fit a persistent side panel:
Codex Panel supports Codex App Server workflows that fit a persistent side panel:
- Start, resume, rename, fork, roll back, compact, and archive threads (`/new`, `/resume`, `/fork`, `/rollback`, `/compact`, `/archive`).
- Complete slash commands (`/help`) and enabled Codex skills from the composer.
- Reference another non-archived thread without switching away from the current one (`/refer <thread> <message>`). Codex receives up to 20 recent turns, limited to user input and final Codex responses.
- Toggle Plan mode, optionally sending the same message after switching (`/plan <message>`), then answer questions and copy or implement proposed plans.
- Toggle fast mode, model override, and reasoning effort override for subsequent turns (`/fast`, `/model`, `/effort`).
- Toggle fast mode, model, and reasoning effort for subsequent turns (`/fast`, `/model`, `/effort`).
- Toggle approval auto-review for subsequent turns (`/auto-review`).
- Send steering messages during a running turn, or interrupt when the composer is empty.
- Respond to command, file, and permission approval requests.
- Stream assistant messages, reasoning, commands, tool calls, hooks, file changes, and agent activity.
- Codex goal management is not supported. Incoming goal notifications are shown only as brief system notices.
- Inspect file changes and roll back the latest turn without reverting local files.
- Inspect context usage, usage limits, connection diagnostics, MCP server inventory, and effective config (`/status`, `/doctor`, `/mcp`).
- Inspect context usage, usage limits, connection diagnostics, MCP servers, and effective config (`/status`, `/doctor`, `/mcp`).
- Inspect and manage discovered Codex hooks from Codex Panel settings.
- Rewrite the current Markdown editor selection from an Obsidian editor command.
## Obsidian Integration
## Obsidian integration
Codex Panel adds vault-aware behavior where Obsidian benefits from a different surface than the terminal UI:
- Wikilinks in sent messages resolve to Codex file mentions when the target file exists. The visible message text is preserved, unresolved wikilinks are left alone, and note bodies are not attached automatically.
- Markdown links in rendered messages that point to existing vault files open in Obsidian. External links and non-vault file paths keep their normal link behavior.
- Forking a thread opens the fork in a new right-sidebar panel so the source thread stays visible.
- Thread archiving can save a Markdown note in the vault before the thread is archived. The save location, filename template, and optional fixed tags are configured in Codex Panel settings.
- Thread archiving can save a Markdown note in the vault before the thread is archived. Configure the folder, filename template, and optional fixed tags in Codex Panel settings.
- The composer sends with `Enter` by default, with `Shift+Enter` for a newline. You can switch sending to `Cmd/Ctrl+Enter` in Codex Panel settings.
- `Codex Panel: Rewrite selection` rewrites selected text in the active Markdown editor without starting a normal chat turn. The command requires a non-empty selection in an active Markdown note. Codex sees the selection, the current editor buffer as note context, and your instruction, then returns a replacement preview with a selection-scoped diff.
- Selection rewrites run in a short-lived app-server session, separate from the current panel thread. Nothing is written to the note until you confirm the preview. If the selected text changes before applying, Codex Panel asks you to regenerate so the replacement still matches the editor state you reviewed.
- **Codex Panel: Rewrite selection** rewrites selected text in the active Markdown editor without starting a normal chat turn. The command requires a non-empty selection in an active Markdown note. Codex sees the selection, the current editor buffer as note context, and your instruction, then returns a replacement preview with a selection-scoped diff.
- Selection rewrites run in a short-lived Codex App Server session, separate from the current panel thread. Nothing is written to the note until you confirm the preview. If the selected text changes before applying, Codex Panel asks you to regenerate so the replacement still matches the editor state you reviewed.
## Configuration and Diagnostics
## Configuration and diagnostics
Codex configuration stays in Codex. Configure model defaults, sandboxing, approvals, MCP servers, hooks, network access, and providers the same way you configure Codex CLI.
Codex configuration stays in Codex. Configure model defaults, reasoning effort, sandbox modes, approval policies, MCP servers, hooks, network access, and providers the same way you configure Codex CLI.
Codex Panel settings store only panel-specific preferences: Codex executable path, composer send shortcut, optional archive export folder and filename templates, optional archive export tags, and optional model/effort overrides for automatic thread naming and selection rewrites. Toolbar model and effort controls are temporary inputs for subsequent turns.
Codex Panel settings store only panel-specific preferences: Codex executable path, composer send shortcut, optional archive export folder and filename templates, optional archive export tags, and optional model and reasoning effort choices for automatic thread naming and selection rewrites. Toolbar model and effort controls are temporary inputs for subsequent turns.
The status dot opens connection controls and diagnostics, including effective config, context usage, and usage limits. Codex Panel settings can also load app-server-backed hook status and archived threads on demand.
The status dot opens connection controls and diagnostics, including effective config, context usage, and usage limits. Codex Panel settings can also load hook status and archived threads from Codex App Server.
## Privacy and Security
## Privacy and security
Codex Panel does not make its own network requests. It exchanges data with the configured `codex app-server` process over stdio. Messages, steering input, approvals, resolved file mentions, thread history operations, hook status requests, effective config requests, and archived-thread management requests are sent to Codex app-server and handled according to your Codex CLI configuration.
Codex Panel does not make its own network requests. It exchanges data with the configured `codex app-server` process over stdio. Messages, steering input, approvals, resolved file mentions, thread history operations, hook status requests, effective config requests, and archived-thread management requests are sent to Codex App Server and handled according to your Codex CLI configuration.
The plugin does not store API keys.
@ -88,9 +88,9 @@ The plugin does not store API keys.
| `obsidian.minAppVersion` | `1.12.0` | Track the latest patch in this minor; raise this baseline when adopting a newer Obsidian API minor. |
| `codex.testedCliVersion` | `0.133.0` | Manage app-server compatibility by Codex CLI minor version. |
Codex Panel depends on the experimental `codex app-server` API. Later Codex CLI releases may require regenerated app-server bindings or compatibility fixes.
Codex Panel depends on the experimental `codex app-server` API. Later Codex CLI releases may require regenerated App Server bindings or compatibility fixes.
## Project Docs
## Project docs
- [Development](docs/development.md)
- [Release](docs/release.md)

View file

@ -3,7 +3,7 @@
"name": "Codex Panel",
"version": "1.2.1",
"minAppVersion": "1.12.0",
"description": "Codex in your sidebar.",
"description": "Run Codex from Obsidian's right sidebar.",
"author": "murashit",
"isDesktopOnly": true
}

View file

@ -1,17 +1,17 @@
export const SLASH_COMMANDS = [
{ command: "/new", detail: "Start a new Codex thread, optionally sending a message." },
{ command: "/new", detail: "Start a new Codex thread, optionally with a message." },
{ command: "/resume", detail: "Resume a recent Codex thread." },
{ command: "/refer", detail: "Send a message with up to 20 recent turns from another non-archived thread." },
{ command: "/refer", detail: "Send a message with recent turns from another non-archived thread." },
{ command: "/fork", detail: "Fork the active Codex thread." },
{ command: "/rollback", detail: "Drop the latest turn and restore its prompt to the composer." },
{ command: "/rollback", detail: "Roll back the latest turn and restore its prompt." },
{ command: "/compact", detail: "Compact the current conversation context." },
{ command: "/archive", detail: "Archive the active or selected Codex thread." },
{ command: "/auto-review", detail: "Toggle approval auto-review, optionally sending a message." },
{ command: "/auto-review", detail: "Toggle approval auto-review, optionally with a message." },
{ command: "/fast", detail: "Toggle fast service tier for subsequent turns." },
{ command: "/plan", detail: "Toggle Plan mode, optionally sending a message." },
{ command: "/plan", detail: "Toggle Plan mode, optionally with a message." },
{ command: "/status", detail: "Show current session, context, and usage limits." },
{ command: "/doctor", detail: "Show Codex CLI and app-server connection diagnostics." },
{ command: "/mcp", detail: "Show MCP servers recognized by Codex app-server." },
{ command: "/doctor", detail: "Show Codex CLI and Codex App Server diagnostics." },
{ command: "/mcp", detail: "Show MCP servers reported by Codex App Server." },
{ command: "/model", detail: "Show or set the model for subsequent turns." },
{ command: "/effort", detail: "Show or set reasoning effort for subsequent turns." },
{ command: "/help", detail: "Show available Codex slash commands." },

View file

@ -25,7 +25,7 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH
name: "Rewrite selection",
editorCallback: (editor, view) => {
if (!(view instanceof MarkdownView) || !view.file) {
new Notice("Rewrite selection requires an active markdown note.");
new Notice("Select text in an active markdown note first.");
return;
}

View file

@ -157,7 +157,7 @@ export class RewriteSelectionPopover {
const instruction = root.createEl("textarea", {
cls: "codex-panel__input codex-panel-rewrite-popover__instruction",
attr: { placeholder: "How should Codex rewrite the selected text?" },
attr: { placeholder: "How should Codex rewrite this selection?" },
});
instruction.value = this.options.session.instruction;
instruction.oninput = () => {
@ -179,16 +179,11 @@ export class RewriteSelectionPopover {
const generateButton = createIconButton(
controls,
"sparkles",
"Generate rewrite",
"Generate",
"codex-panel__composer-action codex-panel-rewrite-popover__icon-button",
);
generateButton.onclick = () => void this.generate();
const cancelButton = createIconButton(
controls,
"x",
"Cancel rewrite",
"codex-panel__composer-action codex-panel-rewrite-popover__icon-button",
);
const cancelButton = createIconButton(controls, "x", "Cancel", "codex-panel__composer-action codex-panel-rewrite-popover__icon-button");
cancelButton.onclick = () => {
this.cancel();
};
@ -200,7 +195,7 @@ export class RewriteSelectionPopover {
const applyButton = createIconButton(
resultRow,
"check",
"Apply rewrite",
"Apply",
"codex-panel__composer-action codex-panel-rewrite-popover__icon-button mod-cta",
);
applyButton.onclick = () => {
@ -317,7 +312,7 @@ export class RewriteSelectionPopover {
const hasReplacement = this.options.session.replacementText !== null;
this.elements.instruction.disabled = generating;
this.elements.generateButton.disabled = generating || !hasInstruction;
this.elements.generateButton.setAttr("aria-label", hasReplacement ? "Regenerate rewrite" : "Generate rewrite");
this.elements.generateButton.setAttr("aria-label", hasReplacement ? "Regenerate" : "Generate");
this.elements.applyButton.disabled = generating || !hasReplacement;
this.elements.applyButton.classList.toggle("is-hidden", !hasReplacement);
this.elements.resultRow.classList.toggle("is-hidden", !hasReplacement);

View file

@ -32,10 +32,10 @@ export function connectionDiagnosticSections(input: ConnectionDiagnosticsInput):
rows: [
{ label: "connection", value: input.connected ? "connected" : "offline" },
{ label: "configured command", value: input.configuredCommand },
{ label: "running app-server", value: appServerIdentity(input.initializeResponse) },
{ label: "Codex App Server", value: appServerIdentity(input.initializeResponse) },
{ label: "panel client", value: CLIENT_VERSION },
{ label: "platform", value: appServerPlatform(input.initializeResponse) },
{ label: "codexHome", value: input.initializeResponse?.codexHome ?? "(not connected)" },
{ label: "Codex home", value: input.initializeResponse?.codexHome ?? "(not connected)" },
{ label: "active thread CLI", value: input.activeThreadCliVersion ?? "(none)" },
],
},

View file

@ -3,7 +3,7 @@ import type { McpServerDiagnostic } from "../app-server/compatibility";
export function mcpStatusLines(servers: McpServerStatus[], diagnostics: McpServerDiagnostic[] = []): string[] {
if (servers.length === 0 && diagnostics.length === 0) {
return ["MCP servers", "Codex app-server reports no MCP servers."];
return ["MCP servers", "Codex App Server reports no MCP servers."];
}
const statusByName = new Map(servers.map((server) => [server.name, server]));

View file

@ -160,7 +160,7 @@ export function effectiveConfigSections(snapshot: RuntimeSnapshot, vaultPath: st
title: "Features",
rows: [
{ key: "hooks", value: stringValue(config.hooksEnabled, "(from default)") },
{ key: "apply patch freeform", value: stringValue(config.applyPatchFreeformEnabled, "(from default)") },
{ key: "apply_patch freeform", value: stringValue(config.applyPatchFreeformEnabled, "(from default)") },
{ key: "tool web search", value: stringValue(config.toolWebSearch, "(from default)") },
{ key: "apps", value: enabledAppsLabel(config.apps) },
],

View file

@ -38,7 +38,7 @@ export function renderHookSection(containerEl: HTMLElement, state: HookSectionSt
.setClass("codex-panel-settings__dynamic-section-heading")
.setHeading()
.setName("Hook status")
.setDesc("Review hooks discovered for this vault root, including trust and enabled state.");
.setDesc("Review discovered hooks, trust changes, and turn hooks on or off.");
if (state.loading) {
section.createEl("p", { cls: "setting-item-description codex-panel-settings__dynamic-section-status", text: "Loading hooks..." });
@ -57,7 +57,7 @@ export function renderArchivedThreadSection(containerEl: HTMLElement, state: Arc
.setClass("codex-panel-settings__dynamic-section-heading")
.setHeading()
.setName("Archive actions")
.setDesc("Configure what happens when a thread is archived, and restore already archived threads when needed.");
.setDesc("Save threads to notes before archiving, or restore archived threads.");
renderArchiveExportSettings(section, state);
@ -85,7 +85,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
new Setting(containerEl)
.setName("Save before archiving")
.setDesc(
"Save a markdown note before archiving. If saving fails, the thread is not archived. Frontmatter includes title, thread_id, created, and optional tags.",
"Save a markdown note before archiving. If saving fails, the thread stays active. Frontmatter includes title, thread_id, created, and optional tags.",
)
.addToggle((toggle) => {
toggle.setValue(state.exportEnabled).onChange((value) => {
@ -95,7 +95,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
new Setting(containerEl)
.setName("Save folder")
.setDesc("Vault-relative folder path. The folder is created when needed.")
.setDesc("Vault-relative folder for saved thread notes. The folder is created when needed.")
.addText((text) => {
text
.setPlaceholder("Codex archives")
@ -107,9 +107,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
new Setting(containerEl)
.setName("Save filename")
.setDesc(
"Markdown filename template. Available variables: {{date}}, {{time}}, {{title}}, {{id}}, {{shortId}}. Existing files get a numeric suffix.",
)
.setDesc("Filename template. Variables: {{date}}, {{time}}, {{title}}, {{id}}, {{shortId}}. Existing files get a numeric suffix.")
.addText((text) => {
text
.setPlaceholder("{{date}} {{time}} {{title}} {{shortId}}.md")
@ -121,7 +119,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
new Setting(containerEl)
.setName("Save tags")
.setDesc("Comma-separated fixed tags for saved notes. Variables are not expanded. Leave empty to omit tags.")
.setDesc("Comma-separated fixed tags for saved notes. Leave empty to omit tags.")
.addText((text) => {
text
.setPlaceholder("Codex, archive")
@ -135,9 +133,9 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
function renderArchivedThreadList(containerEl: HTMLElement, state: ArchivedThreadSectionState): void {
containerEl.createEl("p", {
cls: "setting-item-description codex-panel-settings__dynamic-list-summary",
text: `Archived threads below can be restored to chat history. Loaded ${String(state.threads.length)} archived thread${
text: `Restore archived threads to chat history. Loaded ${String(state.threads.length)} archived thread${
state.threads.length === 1 ? "" : "s"
} from Codex app-server.`,
} from Codex app server.`,
});
const list = containerEl.createDiv({ cls: "setting-items codex-panel-settings__dynamic-list codex-panel-settings__archived-list" });
for (const thread of state.threads) {
@ -160,11 +158,11 @@ function renderArchivedThreadList(containerEl: HTMLElement, state: ArchivedThrea
function renderHooks(containerEl: HTMLElement, state: HookSectionState): void {
if (state.hooks.length === 0) {
containerEl.createEl("p", { cls: "setting-item-description", text: "No hooks discovered for the current vault root." });
containerEl.createEl("p", { cls: "setting-item-description", text: "No hooks found for this vault root." });
} else {
containerEl.createEl("p", {
cls: "setting-item-description codex-panel-settings__dynamic-list-summary",
text: `Loaded ${String(state.hooks.length)} hook${state.hooks.length === 1 ? "" : "s"} from Codex app-server.`,
text: `Loaded ${String(state.hooks.length)} hook${state.hooks.length === 1 ? "" : "s"} from Codex app server.`,
});
const list = containerEl.createDiv({ cls: "setting-items codex-panel-settings__dynamic-list codex-panel-settings__hook-list" });
for (const hook of state.hooks) {

View file

@ -20,6 +20,10 @@ const SEND_SHORTCUT_LABELS = {
"mod-enter": "Cmd/Ctrl+Enter",
} as const;
function renderSettingsHeading(containerEl: HTMLElement, name: string): void {
new Setting(containerEl).setClass("codex-panel-settings__section-heading").setHeading().setName(name);
}
export class CodexPanelSettingTab extends PluginSettingTab {
private settingsDataAutoLoadStarted = false;
private settingsDataLoading = false;
@ -50,27 +54,14 @@ export class CodexPanelSettingTab extends PluginSettingTab {
containerEl.addClass("codex-panel-settings");
const configSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__general-section" });
new Setting(configSection)
.setClass("codex-panel-settings__section-heading")
.setHeading()
.setName("General")
.setDesc(
"Codex Panel stores only panel-specific preferences. Sandbox, approvals, MCP, and normal chat runtime policy still come from Codex config for this vault.",
);
new Setting(configSection)
.setName("Settings data")
.setDesc("Refresh models, hooks, and archived threads from Codex app-server.")
.addButton((button) => {
button
.setButtonText(this.settingsDataLoading ? "Refreshing..." : "Refresh settings data")
.setDisabled(this.settingsDataLoading)
.onClick(() => void this.refreshSettingsData());
});
configSection.createEl("p", {
cls: "setting-item-description codex-panel-settings__section-intro",
text: "Codex Panel stores only panel preferences. Models, sandboxing, approvals, MCP servers, hooks, and network access still come from Codex config.",
});
new Setting(configSection)
.setName("Codex executable")
.setDesc("Command used to launch `codex app-server`. Use an absolute path if Obsidian cannot see your shell PATH.")
.setDesc("Path used to start `codex app-server`. Use an absolute path if Obsidian cannot find `codex`.")
.addText((text) => {
text
.setPlaceholder(DEFAULT_CODEX_PATH)
@ -82,9 +73,21 @@ export class CodexPanelSettingTab extends PluginSettingTab {
});
new Setting(configSection)
.setName("Codex data")
.setDesc("Refresh models, hooks, and archived threads from Codex app server.")
.addButton((button) => {
button
.setButtonText(this.settingsDataLoading ? "Refreshing..." : "Refresh Codex data")
.setDisabled(this.settingsDataLoading)
.onClick(() => void this.refreshSettingsData());
});
const composerSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__composer-section" });
renderSettingsHeading(composerSection, "Composer");
new Setting(composerSection)
.setName("Send shortcut")
.setDesc(
"Choose how the composer sends messages. With Enter selected, Shift+Enter inserts a newline. Obsidian Hotkeys may intercept Cmd/Ctrl+Enter.",
"Choose how the composer sends messages. Shift+Enter inserts a newline when Enter sends. Obsidian hotkeys may intercept Cmd/Ctrl+Enter.",
)
.addDropdown((dropdown) => {
dropdown.selectEl.ariaLabel = "Send shortcut";
@ -97,9 +100,11 @@ export class CodexPanelSettingTab extends PluginSettingTab {
});
});
new Setting(configSection)
const helperSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__helper-section" });
renderSettingsHeading(helperSection, "Codex helpers");
new Setting(helperSection)
.setName("Automatic thread naming")
.setDesc("Model and reasoning effort used only when generating thread title suggestions.")
.setDesc("Choose the model and reasoning effort used to suggest thread names.")
.addDropdown((dropdown) => {
const current = this.plugin.settings.threadNamingModel;
const options = this.modelOptions();
@ -134,9 +139,9 @@ export class CodexPanelSettingTab extends PluginSettingTab {
});
});
new Setting(configSection)
new Setting(helperSection)
.setName("Selection rewrite")
.setDesc("Model and reasoning effort used only by the rewrite selection command.")
.setDesc("Choose the model and reasoning effort used by rewrite selection.")
.addDropdown((dropdown) => {
const current = this.plugin.settings.rewriteSelectionModel;
const options = this.modelOptions();
@ -177,17 +182,6 @@ export class CodexPanelSettingTab extends PluginSettingTab {
});
}
renderHookSection(containerEl, {
hooks: this.hooks,
warnings: this.hookWarnings,
errors: this.hookErrors,
loaded: this.hooksLoaded,
loading: this.hooksLoading,
status: this.hooksStatus,
onTrust: (hook) => void this.trustHook(hook),
onToggleEnabled: (hook, enabled) => void this.setHookEnabled(hook, enabled),
});
renderArchivedThreadSection(containerEl, {
exportEnabled: this.plugin.settings.archiveExportEnabled,
exportFolderTemplate: this.plugin.settings.archiveExportFolderTemplate,
@ -204,6 +198,17 @@ export class CodexPanelSettingTab extends PluginSettingTab {
onRestore: (threadId) => void this.restoreArchivedThread(threadId),
});
renderHookSection(containerEl, {
hooks: this.hooks,
warnings: this.hookWarnings,
errors: this.hookErrors,
loaded: this.hooksLoaded,
loading: this.hooksLoading,
status: this.hooksStatus,
onTrust: (hook) => void this.trustHook(hook),
onToggleEnabled: (hook, enabled) => void this.setHookEnabled(hook, enabled),
});
this.maybeAutoLoadSettingsData();
}
@ -266,7 +271,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
this.archivedThreadsLoading = false;
this.hooksLoading = false;
if (failedCount > 0) {
new Notice("Could not refresh all Codex settings data.");
new Notice("Could not refresh all Codex data.");
}
this.display();
}

View file

@ -90,7 +90,7 @@ function renderUserInputCard(
info.createDiv({ cls: "setting-item-name codex-panel__pending-request-title", text: "Codex needs input" });
info.createDiv({
cls: "setting-item-description codex-panel__pending-request-body",
text: `${String(input.params.questions.length)} question${input.params.questions.length === 1 ? "" : "s"} from Plan mode.`,
text: `Answer ${String(input.params.questions.length)} Plan mode question${input.params.questions.length === 1 ? "" : "s"} to continue.`,
});
renderUserInputQuestions(info, input, drafts);
@ -164,7 +164,7 @@ function renderOtherUserInputOption(
label.createSpan({ cls: "codex-panel__user-input-option-label", text: "Other" });
const inputEl = label.createEl("input", {
cls: "codex-panel__user-input-text codex-panel__user-input-other-text",
attr: { type: "text", value: otherValue, placeholder: "Other..." },
attr: { type: "text", value: otherValue, placeholder: "Other answer" },
});
inputEl.oninput = () => {
drafts.values.set(otherKey, inputEl.value);

View file

@ -276,7 +276,7 @@ function renderRateLimitPanel(parent: HTMLElement, rateLimit: RateLimitSummary |
if (!rateLimit) return;
const section = parent.createDiv({ cls: `codex-panel__limit-panel codex-panel__limit-panel--${rateLimit.level}` });
section.createDiv({ cls: "codex-panel__limit-panel-title", text: "Usage Limit" });
section.createDiv({ cls: "codex-panel__limit-panel-title", text: "Usage limit" });
const list = section.createDiv({ cls: "codex-panel__limit-panel-list" });
for (const row of rateLimit.rows) {
const item = list.createDiv({
@ -311,12 +311,12 @@ function renderConnectionDiagnostics(parent: HTMLElement, sections: ToolbarDiagn
}
function renderRuntimePicker(parent: HTMLElement, model: ToolbarViewModel): void {
const picker = parent.createDiv({ cls: "codex-panel__runtime-picker", attr: { role: "listbox", "aria-label": "Runtime settings" } });
const picker = parent.createDiv({ cls: "codex-panel__runtime-picker", attr: { role: "listbox", "aria-label": "Runtime controls" } });
picker.createDiv({ cls: "codex-panel__runtime-picker-label", text: "Model" });
for (const choice of model.modelChoices) {
createToolbarPanelItem(picker, choice.label, { ...choice, className: "codex-panel__runtime-choice" });
}
picker.createDiv({ cls: "codex-panel__runtime-picker-label", text: "Effort" });
picker.createDiv({ cls: "codex-panel__runtime-picker-label", text: "Reasoning effort" });
for (const choice of model.effortChoices) {
createToolbarPanelItem(picker, choice.label, { ...choice, className: "codex-panel__runtime-choice" });
}

View file

@ -333,26 +333,24 @@ describe("slash commands", () => {
});
it("documents that /plan can take a message", () => {
expect(slashCommandHelpLines().find((line) => line.startsWith("/plan"))).toBe(
"/plan - Toggle Plan mode, optionally sending a message.",
);
expect(slashCommandHelpLines().find((line) => line.startsWith("/plan"))).toBe("/plan - Toggle Plan mode, optionally with a message.");
});
it("documents that /auto-review can take a message", () => {
expect(slashCommandHelpLines().find((line) => line.startsWith("/auto-review"))).toBe(
"/auto-review - Toggle approval auto-review, optionally sending a message.",
"/auto-review - Toggle approval auto-review, optionally with a message.",
);
});
it("documents rollback", () => {
expect(slashCommandHelpLines().find((line) => line.startsWith("/rollback"))).toBe(
"/rollback - Drop the latest turn and restore its prompt to the composer.",
"/rollback - Roll back the latest turn and restore its prompt.",
);
});
it("documents refer history size", () => {
expect(slashCommandHelpLines().find((line) => line.startsWith("/refer"))).toBe(
"/refer - Send a message with up to 20 recent turns from another non-archived thread.",
"/refer - Send a message with recent turns from another non-archived thread.",
);
});
@ -361,7 +359,7 @@ describe("slash commands", () => {
"/status - Show current session, context, and usage limits.",
);
expect(slashCommandHelpLines().find((line) => line.startsWith("/doctor"))).toBe(
"/doctor - Show Codex CLI and app-server connection diagnostics.",
"/doctor - Show Codex CLI and Codex App Server diagnostics.",
);
});
@ -384,6 +382,6 @@ describe("slash commands", () => {
});
it("documents MCP status", () => {
expect(slashCommandHelpLines().find((line) => line.startsWith("/mcp"))).toBe("/mcp - Show MCP servers recognized by Codex app-server.");
expect(slashCommandHelpLines().find((line) => line.startsWith("/mcp"))).toBe("/mcp - Show MCP servers reported by Codex App Server.");
});
});

View file

@ -19,7 +19,7 @@ function mcpServer(overrides: Partial<McpServerStatus> = {}): McpServerStatus {
describe("mcpStatusLines", () => {
it("reports no configured servers clearly", () => {
expect(mcpStatusLines([])).toEqual(["MCP servers", "Codex app-server reports no MCP servers."]);
expect(mcpStatusLines([])).toEqual(["MCP servers", "Codex App Server reports no MCP servers."]);
});
it("formats recognized MCP servers", () => {

View file

@ -85,20 +85,21 @@ describe("settings tab", () => {
await flushPromises();
expect(withAppServerSessionMock).toHaveBeenCalledTimes(1);
expect(buttonTexts(tab)).toContain("Refresh settings data");
expect(buttonTexts(tab)).toContain("Refresh Codex data");
expect(buttonTexts(tab)).not.toContain("Load models");
expect(buttonTexts(tab)).not.toContain("Load hooks");
expect(buttonTexts(tab)).not.toContain("Load archive list");
expect(tab.containerEl.querySelector("h2")).toBeNull();
expect(settingNames(tab)).toEqual([
"General",
"Settings data",
"Codex executable",
"Codex data",
"Composer",
"Send shortcut",
"Codex helpers",
"Automatic thread naming",
"Selection rewrite",
"Hook status",
"Archive actions",
"Hook status",
]);
});
@ -115,8 +116,8 @@ describe("settings tab", () => {
await flushPromises();
expect(saveSettings).toHaveBeenCalledOnce();
expect(settingDesc(tab, "Send shortcut")).toContain("Obsidian Hotkeys");
expect(tab.containerEl.querySelector(".codex-panel-settings__section-status")?.textContent ?? "").not.toContain("Obsidian Hotkeys");
expect(settingDesc(tab, "Send shortcut")).toContain("Obsidian hotkeys");
expect(tab.containerEl.querySelector(".codex-panel-settings__section-status")?.textContent ?? "").not.toContain("Obsidian hotkeys");
});
it("saves archive export settings", async () => {
@ -143,7 +144,7 @@ describe("settings tab", () => {
expect(saveSettings).toHaveBeenCalledTimes(4);
expect(tab.containerEl.textContent).toContain("title, thread_id, created, and optional tags");
expect(settingDesc(tab, "Save before archiving")).toContain("If saving fails");
expect(settingDesc(tab, "Save tags")).toContain("Variables are not expanded");
expect(settingDesc(tab, "Save tags")).toContain("Leave empty to omit tags");
});
it("refreshes models, hooks, and archived threads from the global refresh button", async () => {
@ -163,7 +164,7 @@ describe("settings tab", () => {
tab.display();
await flushPromises();
clickButton(tab, "Refresh settings data");
clickButton(tab, "Refresh Codex data");
await flushPromises();
expect(withAppServerSessionMock).toHaveBeenCalledTimes(2);
@ -189,7 +190,7 @@ describe("settings tab", () => {
expect(tab.containerEl.textContent).not.toContain("Loaded 1 model.");
expect(tab.containerEl.textContent).toContain("Could not load hooks: hooks unavailable");
expect(tab.containerEl.textContent).toContain("Archived thread");
expect(notices).toEqual(["Could not refresh all Codex settings data."]);
expect(notices).toEqual(["Could not refresh all Codex data."]);
});
it("renders archived threads and hooks as dynamic setting rows", async () => {
@ -205,9 +206,9 @@ describe("settings tab", () => {
tab.display();
await flushPromises();
expect(tab.containerEl.textContent).toContain("Loaded 1 hook from Codex app-server.");
expect(tab.containerEl.textContent).toContain("Archived threads below can be restored to chat history.");
expect(tab.containerEl.textContent).toContain("Loaded 1 archived thread from Codex app-server.");
expect(tab.containerEl.textContent).toContain("Loaded 1 hook from Codex app server.");
expect(tab.containerEl.textContent).toContain("Restore archived threads to chat history.");
expect(tab.containerEl.textContent).toContain("Loaded 1 archived thread from Codex app server.");
expect(tab.containerEl.querySelector(".codex-panel-settings__hook-section .setting-item-heading")?.textContent).toContain(
"Hook status",
);
@ -341,10 +342,10 @@ function settingNames(tab: CodexPanelSettingTab): string[] {
if (element.classList.contains("setting-item")) {
return [element.querySelector(".setting-item-name")?.textContent ?? ""];
}
if (element.classList.contains("codex-panel-settings__general-section")) {
return Array.from(element.querySelectorAll(":scope > .setting-item")).map(
(setting) => setting.querySelector(".setting-item-name")?.textContent ?? "",
);
if (element.classList.contains("codex-panel-settings__section")) {
return Array.from(element.querySelectorAll(":scope > .setting-item")).map((setting) => {
return setting.querySelector(".setting-item-name")?.textContent ?? "";
});
}
if (element.classList.contains("codex-panel-settings__dynamic-section")) {
return [element.querySelector(":scope > .setting-item-heading .setting-item-name")?.textContent ?? ""];

View file

@ -1680,7 +1680,7 @@ describe("toolbar renderer decisions", () => {
statusPanelOpen: true,
openPanel: "status",
diagnostics: [
{ title: "Process", rows: [{ label: "running app-server", value: "codex-cli/1.2.3" }] },
{ title: "Process", rows: [{ label: "Codex App Server", value: "codex-cli/1.2.3" }] },
{ title: "Capabilities", rows: [{ label: "compatibility", value: "model/list failed", level: "error" }] },
],
}),
@ -2210,7 +2210,7 @@ function toolbarModel(overrides: Partial<ToolbarViewModel> = {}): ToolbarViewMod
modelChoices: [{ label: "Default", selected: true, onClick: vi.fn() }],
effortChoices: [{ label: "Default", selected: true, onClick: vi.fn() }],
connectLabel: "Reconnect",
diagnostics: [{ title: "Process", rows: [{ label: "running app-server", value: "codex-cli/test" }] }],
diagnostics: [{ title: "Process", rows: [{ label: "Codex App Server", value: "codex-cli/test" }] }],
diagnosticAlertLevel: "normal",
...overrides,
};