mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Expose referenced thread metadata in panel messages
This commit is contained in:
parent
eb5dfd0340
commit
098ce3c0e2
7 changed files with 71 additions and 38 deletions
23
README.md
23
README.md
|
|
@ -40,12 +40,13 @@ Use `Codex Panel: New chat` to start a fresh thread. Use `Codex Panel: Open new
|
|||
|
||||
Codex Panel supports the Codex workflows that are useful from a side panel:
|
||||
|
||||
- Start, resume, rename, fork, roll back, compact, archive, and reference threads (`/new`, `/resume`, `/refer`, `/fork`, `/rollback`, `/compact`).
|
||||
- Start, resume, rename, fork, roll back, compact, and archive threads (`/new`, `/resume`, `/fork`, `/rollback`, `/compact`).
|
||||
- Complete slash commands (`/help`) and enabled Codex skills in 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`).
|
||||
- Send steering messages during a running turn, or interrupt when the composer is empty.
|
||||
- Toggle Plan mode, fast mode, model override, and reasoning effort override for subsequent turns (`/plan`, `/fast`, `/model`, `/effort`).
|
||||
- Respond to command, file, and permission approval requests.
|
||||
- Answer Plan mode questions and copy proposed plans.
|
||||
- Stream assistant messages, reasoning, commands, tool calls, hooks, file changes, and agent activity.
|
||||
- Inspect file changes and roll back the latest turn without reverting local files.
|
||||
- Inspect context usage, usage limits, connection diagnostics, and effective config (`/status`, `/doctor`).
|
||||
|
|
@ -57,29 +58,23 @@ Codex Panel supports the Codex workflows that are useful from a side panel:
|
|||
Codex Panel makes a few Obsidian-specific adjustments instead of mirroring the terminal UI exactly:
|
||||
|
||||
- Wikilinks in sent messages are resolved 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.
|
||||
- `/refer <thread> <message>` sends a message with up to 20 recent turns from another non-archived thread as extra context. It includes user input and final Codex responses only, without intermediate tool calls or command output.
|
||||
- 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.
|
||||
- Rolling back is limited to thread history. It restores the rolled-back prompt to the composer, but it does not revert files changed by Codex.
|
||||
- The composer sends with `Enter` by default, with `Shift+Enter` for a newline. You can switch sending to `Cmd/Ctrl+Enter` in the plugin settings.
|
||||
|
||||
## Selection Rewrites
|
||||
|
||||
Use `Codex Panel: Rewrite selection` when you want Codex to rewrite a specific passage in the active Markdown editor without starting a normal chat turn. Select text, run the command, describe the rewrite you want, review the streamed preview and selection-scoped diff, then apply the replacement.
|
||||
Use `Codex Panel: Rewrite selection` to rewrite selected text in the active Markdown editor without starting a normal chat turn. Codex sees the selection, the current editor buffer as note context, and your instruction; you review the preview and selection-scoped diff before applying.
|
||||
|
||||
Selection rewrites run in a short-lived Codex app-server session instead of the open panel thread. Codex receives the selected text, the current editor buffer as note context, and your instruction, then returns replacement text. Codex Panel applies that replacement through the Obsidian editor only after you confirm it; Codex is not asked to edit the file directly.
|
||||
|
||||
The command stops with a notice when there is no active Markdown note or the selection is empty. Generation can fail if Codex cannot start, the request is interrupted, or the model does not return the expected replacement output. Applying can also fail when the selected range changed after generation; regenerate the rewrite so the replacement is based on the current text.
|
||||
|
||||
The settings tab includes optional model and reasoning effort overrides for selection rewrites. Leave them as Codex default unless you want rewrite requests to use a different runtime from automatic thread naming and normal panel turns.
|
||||
Selection rewrites run in a short-lived app-server session and apply through the Obsidian editor only after confirmation. The command requires a non-empty selection in an active Markdown note, and it asks you to regenerate if the selected text changes before apply.
|
||||
|
||||
## Configuration and Diagnostics
|
||||
|
||||
Codex configuration stays in Codex. Configure defaults and runtime policy such as model, reasoning effort, sandboxing, approvals, MCP servers, hooks, network access, and providers the same way you configure Codex CLI.
|
||||
Codex configuration stays in Codex. Configure model defaults, sandboxing, approvals, MCP servers, hooks, network access, and providers the same way you configure Codex CLI.
|
||||
|
||||
The Codex Panel settings tab stores only panel-specific preferences: Codex executable path, composer send shortcut, and optional model/effort overrides for automatic thread naming and selection rewrites. Model and reasoning effort controls in the panel toolbar are temporary inputs for subsequent turns, not saved Codex defaults.
|
||||
The Codex Panel settings tab stores only panel-specific preferences: Codex executable path, composer send shortcut, and optional model/effort overrides 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. The settings tab can load app-server-backed hook status and archived threads on demand, including hook trust and enabled state. Codex Panel can show and request these app-server operations, but it does not duplicate Codex config as Obsidian settings.
|
||||
The status dot opens connection controls and diagnostics, including effective config, context usage, and usage limits. The settings tab can also load app-server-backed hook status and archived threads on demand.
|
||||
|
||||
## Privacy and Security
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { ReferencedThreadDisplay } from "../threads/reference";
|
||||
|
||||
export type DisplayKind =
|
||||
| "message"
|
||||
| "command"
|
||||
|
|
@ -47,13 +49,6 @@ export interface MessageDisplayItem extends DisplayBase {
|
|||
markdown?: boolean;
|
||||
}
|
||||
|
||||
export interface ReferencedThreadDisplay {
|
||||
threadId: string;
|
||||
title: string;
|
||||
includedTurns: number;
|
||||
turnLimit: number;
|
||||
}
|
||||
|
||||
export interface DisplayTurnDiff {
|
||||
diff: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort";
|
|||
import type { Thread } from "../generated/app-server/v2/Thread";
|
||||
import type { UserInput } from "../generated/app-server/v2/UserInput";
|
||||
import { getThreadTitle } from "../threads/model";
|
||||
import type { ReferencedThreadDisplay } from "../threads/reference";
|
||||
import { slashCommandHelpLines, type SlashCommandName } from "../composer/slash-commands";
|
||||
import {
|
||||
modelOverrideMessage,
|
||||
|
|
@ -16,7 +17,7 @@ export interface SlashCommandExecutionContext {
|
|||
listedThreads: Thread[];
|
||||
startNewThread: () => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
referThread: (thread: Thread, message: string) => Promise<UserInput[] | null>;
|
||||
referThread: (thread: Thread, message: string) => Promise<ThreadReferenceInput | null>;
|
||||
forkThread: (threadId: string) => Promise<void>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
|
|
@ -35,6 +36,12 @@ export interface SlashCommandExecutionContext {
|
|||
export interface SlashCommandExecutionResult {
|
||||
sendText?: string;
|
||||
sendInput?: UserInput[];
|
||||
referencedThread?: ReferencedThreadDisplay;
|
||||
}
|
||||
|
||||
export interface ThreadReferenceInput {
|
||||
input: UserInput[];
|
||||
referencedThread: ReferencedThreadDisplay;
|
||||
}
|
||||
|
||||
export async function executeSlashCommand(
|
||||
|
|
@ -73,9 +80,9 @@ export async function executeSlashCommand(
|
|||
context.addSystemMessage("Use the current thread directly instead of referencing it.");
|
||||
return;
|
||||
}
|
||||
const input = await context.referThread(thread.thread, parsed.message);
|
||||
if (!input) return;
|
||||
return { sendText: parsed.message, sendInput: input };
|
||||
const reference = await context.referThread(thread.thread, parsed.message);
|
||||
if (!reference) return;
|
||||
return { sendText: parsed.message, sendInput: reference.input, referencedThread: reference.referencedThread };
|
||||
}
|
||||
|
||||
if (command === "fork") {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
import { sortedAvailableModels } from "../runtime/model";
|
||||
import { compactContextLabel, modelOverrideMessage, reasoningEffortOverrideMessage } from "../runtime/settings";
|
||||
import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult } from "./slash-commands";
|
||||
import type { ThreadReferenceInput } from "./slash-commands";
|
||||
import { PanelSessionController } from "./session-controller";
|
||||
import { statusValue, usageLimitStatusLines } from "./status-lines";
|
||||
import { ThreadHistoryLoader } from "./thread-history";
|
||||
|
|
@ -50,7 +51,14 @@ import { questionDefaultAnswer, type PendingUserInput } from "../user-input/mode
|
|||
import { PanelComposerController } from "./composer-controller";
|
||||
import { clearActiveThreadState, clearConnectionScopedState, createPanelState, type PanelState } from "./state";
|
||||
import { codexPanelDisplayTitle, getThreadTitle, inheritedForkThreadName, upsertThread } from "../threads/model";
|
||||
import { referencedThreadPrompt, referencedThreadStatus, referencedThreadTurns, REFERENCED_THREAD_TURN_LIMIT } from "../threads/reference";
|
||||
import {
|
||||
referencedThreadDisplay,
|
||||
referencedThreadPrompt,
|
||||
referencedThreadStatus,
|
||||
referencedThreadTurns,
|
||||
REFERENCED_THREAD_TURN_LIMIT,
|
||||
type ReferencedThreadDisplay,
|
||||
} from "../threads/reference";
|
||||
import { renderPendingRequestMessage } from "../ui/pending-request-message";
|
||||
import { renderToolbar, toolbarSignature, type ToolbarChoice, type ToolbarViewModel } from "../ui/toolbar";
|
||||
import type { TurnDiffViewState } from "../ui/turn-diff";
|
||||
|
|
@ -357,7 +365,7 @@ export class CodexPanelView extends ItemView {
|
|||
this.composerController.setDraft("", { clearSuggestions: true });
|
||||
const result = await this.executeSlashCommand(slashCommand.command, slashCommand.args);
|
||||
if (result?.sendText) {
|
||||
await this.sendTurnText(result.sendText, result.sendInput);
|
||||
await this.sendTurnText(result.sendText, result.sendInput, result.referencedThread);
|
||||
}
|
||||
this.render();
|
||||
return;
|
||||
|
|
@ -366,12 +374,12 @@ export class CodexPanelView extends ItemView {
|
|||
await this.sendTurnText(text);
|
||||
}
|
||||
|
||||
private async sendTurnText(text: string, codexInputOverride?: UserInput[]): Promise<void> {
|
||||
private async sendTurnText(text: string, codexInputOverride?: UserInput[], referencedThread?: ReferencedThreadDisplay): Promise<void> {
|
||||
const client = this.client;
|
||||
if (!client) return;
|
||||
|
||||
if (this.state.busy) {
|
||||
await this.steerCurrentTurn(text);
|
||||
await this.steerCurrentTurn(text, codexInputOverride, referencedThread);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -391,6 +399,7 @@ export class CodexPanelView extends ItemView {
|
|||
role: "user",
|
||||
text,
|
||||
copyText: text,
|
||||
...(referencedThread ? { referencedThread } : {}),
|
||||
markdown: true,
|
||||
});
|
||||
this.forceMessagesToBottom();
|
||||
|
|
@ -430,7 +439,11 @@ export class CodexPanelView extends ItemView {
|
|||
this.scheduleRender();
|
||||
}
|
||||
|
||||
private async steerCurrentTurn(text: string): Promise<void> {
|
||||
private async steerCurrentTurn(
|
||||
text: string,
|
||||
codexInputOverride?: UserInput[],
|
||||
referencedThread?: ReferencedThreadDisplay,
|
||||
): Promise<void> {
|
||||
if (!this.client || !this.state.activeThreadId || !this.state.activeTurnId) {
|
||||
this.addSystemMessage("Current turn is not steerable yet.");
|
||||
return;
|
||||
|
|
@ -443,7 +456,7 @@ export class CodexPanelView extends ItemView {
|
|||
this.syncComposerControls();
|
||||
|
||||
try {
|
||||
await this.client.steerTurn(threadId, expectedTurnId, this.composerController.codexInput(text));
|
||||
await this.client.steerTurn(threadId, expectedTurnId, codexInputOverride ?? this.composerController.codexInput(text));
|
||||
this.state.displayItems.push({
|
||||
id: `local-steer-${Date.now()}`,
|
||||
kind: "message",
|
||||
|
|
@ -451,6 +464,7 @@ export class CodexPanelView extends ItemView {
|
|||
text,
|
||||
copyText: text,
|
||||
turnId: expectedTurnId,
|
||||
...(referencedThread ? { referencedThread } : {}),
|
||||
markdown: true,
|
||||
});
|
||||
this.forceMessagesToBottom();
|
||||
|
|
@ -519,7 +533,7 @@ export class CodexPanelView extends ItemView {
|
|||
});
|
||||
}
|
||||
|
||||
private async referencedThreadInput(thread: Thread, message: string): Promise<UserInput[] | null> {
|
||||
private async referencedThreadInput(thread: Thread, message: string): Promise<ThreadReferenceInput | null> {
|
||||
if (!this.client) return null;
|
||||
try {
|
||||
const response = await this.client.threadTurnsList(thread.id, null, REFERENCED_THREAD_TURN_LIMIT);
|
||||
|
|
@ -531,7 +545,10 @@ export class CodexPanelView extends ItemView {
|
|||
const prompt = referencedThreadPrompt(thread, turns, message);
|
||||
const messageInput = this.composerController.codexInput(message);
|
||||
this.setStatus(referencedThreadStatus(thread, turns.length));
|
||||
return [{ type: "text", text: prompt, text_elements: [] }, ...messageInput.filter((item) => item.type !== "text")];
|
||||
return {
|
||||
input: [{ type: "text", text: prompt, text_elements: [] }, ...messageInput.filter((item) => item.type !== "text")],
|
||||
referencedThread: referencedThreadDisplay(thread, turns.length),
|
||||
};
|
||||
} catch (error) {
|
||||
this.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
.setHeading()
|
||||
.setName("General")
|
||||
.setDesc(
|
||||
"This plugin stores only panel metadata, the app-server launch command, composer send shortcut, and optional automatic thread naming runtime overrides. Sandbox, approvals, MCP, and normal chat runtime policy are resolved from Codex config for the current vault.",
|
||||
"This plugin stores only panel metadata, the app-server launch command, composer send shortcut, and optional runtime overrides for automatic thread naming and selection rewrites. Sandbox, approvals, MCP, and normal chat runtime policy are resolved from Codex config for the current vault.",
|
||||
);
|
||||
|
||||
new Setting(configSection)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,16 @@ import type { ThreadItem } from "../generated/app-server/v2/ThreadItem";
|
|||
import type { Turn } from "../generated/app-server/v2/Turn";
|
||||
import { inputToText, shortThreadId } from "../utils";
|
||||
import { getThreadTitle } from "./model";
|
||||
import type { ReferencedThreadDisplay } from "../display/types";
|
||||
|
||||
export const REFERENCED_THREAD_TURN_LIMIT = 20;
|
||||
|
||||
export interface ReferencedThreadDisplay {
|
||||
threadId: string;
|
||||
title: string;
|
||||
includedTurns: number;
|
||||
turnLimit: number;
|
||||
}
|
||||
|
||||
export interface ReferencedThreadTurn {
|
||||
userText: string | null;
|
||||
assistantText: string | null;
|
||||
|
|
@ -54,6 +60,15 @@ export function referencedThreadStatus(thread: Thread, count: number): string {
|
|||
return `Referencing ${shortThreadId(thread.id)} (${count}/${REFERENCED_THREAD_TURN_LIMIT} turns).`;
|
||||
}
|
||||
|
||||
export function referencedThreadDisplay(thread: Thread, count: number): ReferencedThreadDisplay {
|
||||
return {
|
||||
threadId: thread.id,
|
||||
title: getThreadTitle(thread),
|
||||
includedTurns: count,
|
||||
turnLimit: REFERENCED_THREAD_TURN_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
export function referencedThreadDisplayFromPrompt(text: string): { text: string; reference: ReferencedThreadDisplay } | null {
|
||||
const headerStart = text.indexOf("[Codex Panel referenced thread]");
|
||||
const headerEnd = text.indexOf("[/Codex Panel referenced thread]");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCo
|
|||
listedThreads: [thread({ id: "thread-1", name: "Current" })],
|
||||
startNewThread: vi.fn().mockResolvedValue(undefined),
|
||||
resumeThread: vi.fn().mockResolvedValue(undefined),
|
||||
referThread: vi.fn().mockResolvedValue([{ type: "text", text: "referenced", text_elements: [] }]),
|
||||
referThread: vi.fn().mockResolvedValue({
|
||||
input: [{ type: "text", text: "referenced", text_elements: [] }],
|
||||
referencedThread: { threadId: "thread-2", title: "Referenced", includedTurns: 1, turnLimit: 20 },
|
||||
}),
|
||||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
compactThread: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -106,15 +109,16 @@ describe("slash commands", () => {
|
|||
it("returns referenced input for /refer", async () => {
|
||||
const target = thread({ id: "thread-alpha", name: "Alpha" });
|
||||
const input = [{ type: "text" as const, text: "context\n質問です", text_elements: [] }];
|
||||
const referencedThread = { threadId: "thread-alpha", title: "Alpha", includedTurns: 2, turnLimit: 20 };
|
||||
const ctx = context({
|
||||
listedThreads: [thread({ id: "thread-current", name: "Current" }), target],
|
||||
referThread: vi.fn().mockResolvedValue(input),
|
||||
referThread: vi.fn().mockResolvedValue({ input, referencedThread }),
|
||||
});
|
||||
|
||||
const result = await executeSlashCommand("refer", "thread-alpha 質問です", ctx);
|
||||
|
||||
expect(ctx.referThread).toHaveBeenCalledWith(target, "質問です");
|
||||
expect(result).toEqual({ sendText: "質問です", sendInput: input });
|
||||
expect(result).toEqual({ sendText: "質問です", sendInput: input, referencedThread });
|
||||
});
|
||||
|
||||
it("rejects /refer without both thread and message", async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue