mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Align feature naming across UI and tests
This commit is contained in:
parent
9cb71e113b
commit
993e8782b7
48 changed files with 596 additions and 580 deletions
|
|
@ -15,11 +15,11 @@ import { userInputWithWikiLinkMentionsAndSkills } from "./composer/wikilink-cont
|
|||
import type { UserInput } from "../../generated/app-server/v2/UserInput";
|
||||
import type { SendShortcut } from "../../settings/model";
|
||||
import { renderComposerShell, renderComposerSuggestions, syncComposerControls, syncComposerHeight } from "./ui/composer";
|
||||
import type { PanelState } from "./state";
|
||||
import type { ChatState } from "./chat-state";
|
||||
|
||||
export interface PanelComposerControllerOptions {
|
||||
export interface ChatComposerControllerOptions {
|
||||
app: App;
|
||||
state: PanelState;
|
||||
state: ChatState;
|
||||
viewId: string;
|
||||
sendShortcut: () => SendShortcut;
|
||||
canInterrupt: () => boolean;
|
||||
|
|
@ -29,13 +29,13 @@ export interface PanelComposerControllerOptions {
|
|||
onNewThread: () => void;
|
||||
}
|
||||
|
||||
export class PanelComposerController {
|
||||
export class ChatComposerController {
|
||||
private composer: HTMLTextAreaElement | null = null;
|
||||
private suggestionsEl: HTMLElement | null = null;
|
||||
private noteCandidatesCache: NoteCandidate[] | null = null;
|
||||
private noteEventsRegistered = false;
|
||||
|
||||
constructor(private readonly options: PanelComposerControllerOptions) {}
|
||||
constructor(private readonly options: ChatComposerControllerOptions) {}
|
||||
|
||||
get trimmedDraft(): string {
|
||||
return this.composer?.value.trim() ?? this.options.state.composerDraft.trim();
|
||||
|
|
@ -25,7 +25,7 @@ import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
|||
import type { FileUpdateChange } from "../../generated/app-server/v2/FileUpdateChange";
|
||||
import type { ThreadItem } from "../../generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../../generated/app-server/v2/Turn";
|
||||
import { clearActiveThreadState, type PanelState } from "./state";
|
||||
import { clearActiveThreadState, type ChatState } from "./chat-state";
|
||||
import { userInputResponse, type PendingUserInput } from "./user-input/model";
|
||||
import { jsonPreview } from "../../utils";
|
||||
import { classifyAppServerLog } from "./app-server-logs";
|
||||
|
|
@ -33,7 +33,7 @@ import { attachHookRunsToTurn, hookRunDisplayItem } from "./hook-display";
|
|||
import { routeServerNotification, routeServerRequest } from "./inbound-routing";
|
||||
import { clearUserInputDrafts, createApprovalResultItem, createUserInputResultItem } from "./request-state";
|
||||
|
||||
export interface PanelControllerActions {
|
||||
export interface ChatControllerActions {
|
||||
refreshThreads: () => void;
|
||||
refreshSkills: (forceReload?: boolean) => void;
|
||||
maybeNameThread: (threadId: string, turn: Turn) => void;
|
||||
|
|
@ -42,10 +42,10 @@ export interface PanelControllerActions {
|
|||
rejectServerRequest: (requestId: RequestId, code: number, message: string) => boolean;
|
||||
}
|
||||
|
||||
export class PanelController {
|
||||
export class ChatController {
|
||||
constructor(
|
||||
private readonly state: PanelState,
|
||||
private readonly actions: PanelControllerActions,
|
||||
private readonly state: ChatState,
|
||||
private readonly actions: ChatControllerActions,
|
||||
) {}
|
||||
|
||||
handleNotification(notification: ServerNotification): void {
|
||||
|
|
@ -5,28 +5,28 @@ import { copyTextWithNotice } from "../../shared/ui/clipboard";
|
|||
import { renderTextWithWikiLinks as renderInlineWikiLinks } from "../../shared/ui/dom";
|
||||
import { messageRenderBlocks, notifyMessageContentRendered, syncMessageRenderBlocks } from "./ui/message-stream";
|
||||
import { bottomScrollTop, captureScrollAnchor, isNearScrollBottom, restoreScrollAnchor } from "./ui/scroll";
|
||||
import type { TurnDiffViewState } from "./ui/turn-diff";
|
||||
import type { ChatTurnDiffViewState } from "./ui/turn-diff";
|
||||
import { markdownFileLinkTarget } from "./markdown-file-links";
|
||||
import { isRollbackCandidateItem, rollbackCandidateFromItems } from "./rollback";
|
||||
import type { PanelState } from "./state";
|
||||
import type { ChatState } from "./chat-state";
|
||||
|
||||
export interface PanelMessageRendererOptions {
|
||||
export interface ChatMessageRendererOptions {
|
||||
app: App;
|
||||
owner: Component;
|
||||
state: PanelState;
|
||||
state: ChatState;
|
||||
vaultPath: string;
|
||||
blockSignatures: Map<string, string>;
|
||||
consumeForceScrollToBottom: () => boolean;
|
||||
loadOlderTurns: () => void;
|
||||
rollbackThread: (threadId: string) => void;
|
||||
implementPlan: (item: DisplayItem) => void;
|
||||
openTurnDiff: (state: TurnDiffViewState) => void;
|
||||
openTurnDiff: (state: ChatTurnDiffViewState) => void;
|
||||
pendingRequestsSignature: () => string;
|
||||
renderPendingRequests: () => HTMLElement | null;
|
||||
}
|
||||
|
||||
export class PanelMessageRenderer {
|
||||
constructor(private readonly options: PanelMessageRendererOptions) {}
|
||||
export class ChatMessageRenderer {
|
||||
constructor(private readonly options: ChatMessageRendererOptions) {}
|
||||
|
||||
render(parent: HTMLElement): void {
|
||||
const { state } = this.options;
|
||||
|
|
@ -154,7 +154,7 @@ export class PanelMessageRenderer {
|
|||
}
|
||||
|
||||
export function implementPlanCandidateFromState(
|
||||
state: Pick<PanelState, "activeThreadId" | "busy" | "composerDraft" | "requestedCollaborationMode" | "displayItems">,
|
||||
state: Pick<ChatState, "activeThreadId" | "busy" | "composerDraft" | "requestedCollaborationMode" | "displayItems">,
|
||||
): DisplayItem | null {
|
||||
if (!state.activeThreadId || state.busy || state.composerDraft.trim().length > 0 || state.requestedCollaborationMode !== "plan") {
|
||||
return null;
|
||||
|
|
@ -8,18 +8,18 @@ import {
|
|||
import { reportedServiceTier } from "../../app-server/service-tier";
|
||||
import type { McpServerStatus } from "../../generated/app-server/v2/McpServerStatus";
|
||||
import { requestedOrConfiguredServiceTier, type RuntimeSnapshot } from "../../runtime/state";
|
||||
import type { PanelState } from "./state";
|
||||
import type { ChatState } from "./chat-state";
|
||||
|
||||
export interface PanelSessionControllerHost {
|
||||
state: PanelState;
|
||||
export interface ChatSessionControllerHost {
|
||||
state: ChatState;
|
||||
vaultPath: string;
|
||||
currentClient: () => AppServerClient | null;
|
||||
runtimeSnapshot: () => RuntimeSnapshot;
|
||||
forceMessagesToBottom: () => void;
|
||||
}
|
||||
|
||||
export class PanelSessionController {
|
||||
constructor(private readonly host: PanelSessionControllerHost) {}
|
||||
export class ChatSessionController {
|
||||
constructor(private readonly host: ChatSessionControllerHost) {}
|
||||
|
||||
async refreshThreadList(): Promise<void> {
|
||||
const client = this.host.currentClient();
|
||||
|
|
@ -22,7 +22,7 @@ export interface PendingTurnStart {
|
|||
promptSubmitHookItemIds: string[];
|
||||
}
|
||||
|
||||
export interface PanelState {
|
||||
export interface ChatState {
|
||||
status: string;
|
||||
effectiveConfig: ConfigReadResponse | null;
|
||||
initializeResponse: InitializeResponse | null;
|
||||
|
|
@ -66,7 +66,7 @@ export interface PanelState {
|
|||
openDetails: Set<string>;
|
||||
}
|
||||
|
||||
export function createPanelState(): PanelState {
|
||||
export function createChatState(): ChatState {
|
||||
return {
|
||||
status: "Idle",
|
||||
effectiveConfig: null,
|
||||
|
|
@ -112,7 +112,7 @@ export function createPanelState(): PanelState {
|
|||
};
|
||||
}
|
||||
|
||||
export function clearActiveTurnState(state: PanelState): void {
|
||||
export function clearActiveTurnState(state: ChatState): void {
|
||||
state.activeTurnId = null;
|
||||
state.busy = false;
|
||||
state.pendingTurnStart = null;
|
||||
|
|
@ -121,7 +121,7 @@ export function clearActiveTurnState(state: PanelState): void {
|
|||
state.userInputDrafts.clear();
|
||||
}
|
||||
|
||||
export function clearActiveThreadState(state: PanelState): void {
|
||||
export function clearActiveThreadState(state: ChatState): void {
|
||||
state.activeThreadId = null;
|
||||
state.activeThreadCwd = null;
|
||||
state.activeModel = null;
|
||||
|
|
@ -139,7 +139,7 @@ export function clearActiveThreadState(state: PanelState): void {
|
|||
clearActiveTurnState(state);
|
||||
}
|
||||
|
||||
export function clearConnectionScopedState(state: PanelState): void {
|
||||
export function clearConnectionScopedState(state: ChatState): void {
|
||||
clearActiveTurnState(state);
|
||||
state.activeModel = null;
|
||||
state.activeReasoningEffort = null;
|
||||
|
|
@ -3,23 +3,23 @@ import { ItemView, type ViewStateResult } from "obsidian";
|
|||
import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../constants";
|
||||
import { copyTextWithNotice } from "../../shared/ui/clipboard";
|
||||
import {
|
||||
isPersistedTurnDiffViewState,
|
||||
persistedTurnDiffViewState,
|
||||
renderTurnDiffView,
|
||||
type PersistedTurnDiffViewState,
|
||||
type TurnDiffViewState,
|
||||
isPersistedChatTurnDiffViewState,
|
||||
persistedChatTurnDiffViewState,
|
||||
renderChatTurnDiffView,
|
||||
type PersistedChatTurnDiffViewState,
|
||||
type ChatTurnDiffViewState,
|
||||
} from "./ui/turn-diff";
|
||||
|
||||
export class CodexTurnDiffView extends ItemView {
|
||||
private metadata: PersistedTurnDiffViewState | null = null;
|
||||
private payload: TurnDiffViewState | null = null;
|
||||
export class CodexChatTurnDiffView extends ItemView {
|
||||
private metadata: PersistedChatTurnDiffViewState | null = null;
|
||||
private payload: ChatTurnDiffViewState | null = null;
|
||||
|
||||
override getViewType(): string {
|
||||
return VIEW_TYPE_CODEX_TURN_DIFF;
|
||||
}
|
||||
|
||||
override getDisplayText(): string {
|
||||
return "Codex turn diff";
|
||||
return "Codex chat turn diff";
|
||||
}
|
||||
|
||||
override getIcon(): string {
|
||||
|
|
@ -32,7 +32,7 @@ export class CodexTurnDiffView extends ItemView {
|
|||
|
||||
override async setState(state: unknown, result: ViewStateResult): Promise<void> {
|
||||
await super.setState(state, result);
|
||||
this.metadata = isPersistedTurnDiffViewState(state)
|
||||
this.metadata = isPersistedChatTurnDiffViewState(state)
|
||||
? {
|
||||
threadId: state.threadId,
|
||||
turnId: state.turnId,
|
||||
|
|
@ -48,15 +48,15 @@ export class CodexTurnDiffView extends ItemView {
|
|||
this.render();
|
||||
}
|
||||
|
||||
setDiffPayload(payload: TurnDiffViewState): void {
|
||||
this.metadata = persistedTurnDiffViewState(payload);
|
||||
setDiffPayload(payload: ChatTurnDiffViewState): void {
|
||||
this.metadata = persistedChatTurnDiffViewState(payload);
|
||||
this.payload = payload;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const root = this.contentEl;
|
||||
renderTurnDiffView(
|
||||
renderChatTurnDiffView(
|
||||
root,
|
||||
this.payload,
|
||||
this.payload ? { copyDiff: () => void this.copyDiff(this.payload?.diff ?? "") } : {},
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import type { AppServerClient } from "../../app-server/client";
|
||||
import { displayItemsFromTurns } from "./display/thread-items";
|
||||
import type { PanelState } from "./state";
|
||||
import type { ChatState } from "./chat-state";
|
||||
|
||||
export interface ThreadHistoryLoaderHost {
|
||||
state: PanelState;
|
||||
state: ChatState;
|
||||
currentClient: () => AppServerClient | null;
|
||||
render: () => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { AppServerClient } from "../../app-server/client";
|
|||
import type { Thread } from "../../generated/app-server/v2/Thread";
|
||||
import type { Turn } from "../../generated/app-server/v2/Turn";
|
||||
import type { CodexPanelSettings } from "../../settings/model";
|
||||
import type { PanelState } from "./state";
|
||||
import type { ChatState } from "./chat-state";
|
||||
import { getThreadTitle } from "../../domain/threads/model";
|
||||
import {
|
||||
findThreadNamingContext,
|
||||
|
|
@ -19,7 +19,7 @@ export interface ThreadRenameEditState {
|
|||
}
|
||||
|
||||
export interface ThreadRenameControllerHost {
|
||||
state: PanelState;
|
||||
state: ChatState;
|
||||
vaultPath: string;
|
||||
settings: () => CodexPanelSettings;
|
||||
ensureConnected: () => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
renderReasoningItem,
|
||||
renderTaskProgressItem,
|
||||
} from "./work-items";
|
||||
import type { TurnDiffViewState } from "./turn-diff";
|
||||
import type { ChatTurnDiffViewState } from "./turn-diff";
|
||||
|
||||
const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360;
|
||||
const MESSAGE_CONTENT_RENDERED_EVENT = "codex-panel:message-content-rendered";
|
||||
|
|
@ -43,7 +43,7 @@ export interface MessageStreamContext {
|
|||
onImplementPlanItem?: (item: DisplayItem) => void;
|
||||
canRollbackItem?: (item: DisplayItem) => boolean;
|
||||
onRollbackItem?: (item: DisplayItem) => void;
|
||||
openTurnDiff?: (state: TurnDiffViewState) => void;
|
||||
openTurnDiff?: (state: ChatTurnDiffViewState) => void;
|
||||
pendingRequestsSignature?: string;
|
||||
renderPendingRequests?: () => HTMLElement | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function renderDetailSection(parent: HTMLElement, section: ToolResultDetailSecti
|
|||
return;
|
||||
}
|
||||
if (section.kind === "diff") {
|
||||
const diffBlock = renderOutputSection(parent, section.title, "codex-panel__diff-file");
|
||||
const diffBlock = renderOutputSection(parent, section.title, "codex-panel-diff-file");
|
||||
renderDiff(diffBlock, section.diff);
|
||||
return;
|
||||
}
|
||||
|
|
@ -95,9 +95,9 @@ function renderOutputSection(parent: HTMLElement, title: string, className: stri
|
|||
}
|
||||
|
||||
function renderDiff(parent: HTMLElement, diff: string): void {
|
||||
const pre = parent.createEl("pre", { cls: "codex-panel__diff" });
|
||||
const pre = parent.createEl("pre", { cls: "codex-panel-diff" });
|
||||
for (const line of diff.split("\n")) {
|
||||
const cls = diffLineClassFromText(line);
|
||||
pre.createEl("span", { cls: `codex-panel__diff-line codex-panel__diff-line--${cls}`, text: displayDiffLineText(line, cls) });
|
||||
pre.createEl("span", { cls: `codex-panel-diff__line codex-panel-diff__line--${cls}`, text: displayDiffLineText(line, cls) });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { shortThreadId } from "../../../utils";
|
|||
|
||||
export { displayDiffLines } from "../../../shared/diff/unified";
|
||||
|
||||
export interface TurnDiffViewState {
|
||||
export interface ChatTurnDiffViewState {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
cwd: string | null;
|
||||
|
|
@ -12,13 +12,13 @@ export interface TurnDiffViewState {
|
|||
diff: string;
|
||||
}
|
||||
|
||||
export type PersistedTurnDiffViewState = Omit<TurnDiffViewState, "diff">;
|
||||
export type PersistedChatTurnDiffViewState = Omit<ChatTurnDiffViewState, "diff">;
|
||||
|
||||
export interface TurnDiffViewActions {
|
||||
export interface ChatTurnDiffViewActions {
|
||||
copyDiff?: () => void;
|
||||
}
|
||||
|
||||
export function persistedTurnDiffViewState(state: TurnDiffViewState): PersistedTurnDiffViewState {
|
||||
export function persistedChatTurnDiffViewState(state: ChatTurnDiffViewState): PersistedChatTurnDiffViewState {
|
||||
return {
|
||||
threadId: state.threadId,
|
||||
turnId: state.turnId,
|
||||
|
|
@ -27,9 +27,9 @@ export function persistedTurnDiffViewState(state: TurnDiffViewState): PersistedT
|
|||
};
|
||||
}
|
||||
|
||||
export function isPersistedTurnDiffViewState(value: unknown): value is PersistedTurnDiffViewState {
|
||||
export function isPersistedChatTurnDiffViewState(value: unknown): value is PersistedChatTurnDiffViewState {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const record = value as Partial<PersistedTurnDiffViewState>;
|
||||
const record = value as Partial<PersistedChatTurnDiffViewState>;
|
||||
return (
|
||||
typeof record.threadId === "string" &&
|
||||
typeof record.turnId === "string" &&
|
||||
|
|
@ -39,20 +39,20 @@ export function isPersistedTurnDiffViewState(value: unknown): value is Persisted
|
|||
);
|
||||
}
|
||||
|
||||
export function renderTurnDiffView(
|
||||
export function renderChatTurnDiffView(
|
||||
parent: HTMLElement,
|
||||
state: TurnDiffViewState | null,
|
||||
actions: TurnDiffViewActions = {},
|
||||
metadata: PersistedTurnDiffViewState | null = null,
|
||||
state: ChatTurnDiffViewState | null,
|
||||
actions: ChatTurnDiffViewActions = {},
|
||||
metadata: PersistedChatTurnDiffViewState | null = null,
|
||||
): void {
|
||||
parent.empty();
|
||||
parent.addClass("codex-panel-turn-diff");
|
||||
parent.addClass("codex-panel-chat-turn-diff");
|
||||
if (!state) {
|
||||
if (metadata) {
|
||||
renderTurnDiffHeader(parent, metadata, null);
|
||||
parent.createDiv({ cls: "codex-panel-turn-diff__empty", text: "Turn diff is no longer available." });
|
||||
parent.createDiv({ cls: "codex-panel-chat-turn-diff__empty", text: "Turn diff is no longer available." });
|
||||
} else {
|
||||
parent.createDiv({ cls: "codex-panel-turn-diff__empty", text: "No turn diff selected." });
|
||||
parent.createDiv({ cls: "codex-panel-chat-turn-diff__empty", text: "No turn diff selected." });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ export function renderTurnDiffView(
|
|||
renderTurnDiffHeader(parent, state, actions.copyDiff ?? null);
|
||||
|
||||
if (state.files.length > 0) {
|
||||
const files = parent.createEl("details", { cls: "codex-panel-turn-diff__files" });
|
||||
const files = parent.createEl("details", { cls: "codex-panel-chat-turn-diff__files" });
|
||||
files.createEl("summary", { text: "Changed files" });
|
||||
const list = files.createEl("ul");
|
||||
for (const file of state.files) {
|
||||
|
|
@ -73,29 +73,29 @@ export function renderTurnDiffView(
|
|||
|
||||
function renderTurnDiffHeader(
|
||||
parent: HTMLElement,
|
||||
state: PersistedTurnDiffViewState,
|
||||
copyDiff: TurnDiffViewActions["copyDiff"] | null,
|
||||
state: PersistedChatTurnDiffViewState,
|
||||
copyDiff: ChatTurnDiffViewActions["copyDiff"] | null,
|
||||
): void {
|
||||
const header = parent.createDiv({ cls: "codex-panel-turn-diff__header" });
|
||||
const titleBlock = header.createDiv({ cls: "codex-panel-turn-diff__title-block" });
|
||||
titleBlock.createDiv({ cls: "codex-panel-turn-diff__title", text: "Turn diff" });
|
||||
const header = parent.createDiv({ cls: "codex-panel-chat-turn-diff__header" });
|
||||
const titleBlock = header.createDiv({ cls: "codex-panel-chat-turn-diff__title-block" });
|
||||
titleBlock.createDiv({ cls: "codex-panel-chat-turn-diff__title", text: "Turn diff" });
|
||||
titleBlock.createDiv({
|
||||
cls: "codex-panel-turn-diff__meta",
|
||||
cls: "codex-panel-chat-turn-diff__meta",
|
||||
text: `${shortThreadId(state.threadId)} / ${shortThreadId(state.turnId)} · ${fileCountLabel(state.files)}`,
|
||||
attr: { title: `Thread ${state.threadId}\nTurn ${state.turnId}${state.cwd ? `\n${state.cwd}` : ""}` },
|
||||
});
|
||||
if (copyDiff) {
|
||||
const copyButton = createIconButton(header, "copy", "Copy diff", "codex-panel-turn-diff__copy");
|
||||
const copyButton = createIconButton(header, "copy", "Copy diff", "codex-panel-chat-turn-diff__copy");
|
||||
copyButton.onclick = copyDiff;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderUnifiedDiff(parent: HTMLElement, diff: string): HTMLElement {
|
||||
const pre = parent.createEl("pre", { cls: "codex-panel__diff codex-panel-turn-diff__diff" });
|
||||
const pre = parent.createEl("pre", { cls: "codex-panel-diff codex-panel-chat-turn-diff__diff" });
|
||||
for (const line of displayDiffLines(diff)) {
|
||||
const lineClass = diffLineClass(line);
|
||||
pre.createEl("span", {
|
||||
cls: `codex-panel__diff-line codex-panel__diff-line--${lineClass}`,
|
||||
cls: `codex-panel-diff__line codex-panel-diff__line--${lineClass}`,
|
||||
text: displayDiffLineText(line.text, lineClass),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
collaborationModeToggleMessage,
|
||||
nextCollaborationMode,
|
||||
} from "../../runtime/collaboration-mode";
|
||||
import { PanelController } from "./controller";
|
||||
import { ChatController } from "./chat-controller";
|
||||
import { connectionDiagnosticSections, diagnosticAlertLevel } from "./diagnostics";
|
||||
import { rollbackCandidateFromItems } from "./rollback";
|
||||
import { contextSummary, effectiveConfigSections, rateLimitSummary } from "../../runtime/view";
|
||||
|
|
@ -46,16 +46,16 @@ import { compactContextLabel, modelOverrideMessage, reasoningEffortOverrideMessa
|
|||
import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult } from "./slash-commands";
|
||||
import type { ThreadReferenceInput } from "./slash-commands";
|
||||
import { mcpStatusLines } from "./mcp-status";
|
||||
import { PanelSessionController } from "./session-controller";
|
||||
import { ChatSessionController } from "./chat-session-controller";
|
||||
import { statusValue, usageLimitStatusLines } from "./status-lines";
|
||||
import { ThreadHistoryLoader } from "./thread-history";
|
||||
import { ThreadRenameController } from "./thread-rename";
|
||||
import { pendingRequestsSignature as requestStateSignature, userInputDraftKey, userInputOtherDraftKey } from "./request-state";
|
||||
import type { CodexPanelSettings } from "../../settings/model";
|
||||
import { questionDefaultAnswer, type PendingUserInput } from "./user-input/model";
|
||||
import { PanelComposerController } from "./composer-controller";
|
||||
import { ChatComposerController } from "./chat-composer-controller";
|
||||
import { attachHookRunsToTurn } from "./hook-display";
|
||||
import { clearActiveThreadState, clearConnectionScopedState, createPanelState, type PanelState } from "./state";
|
||||
import { clearActiveThreadState, clearConnectionScopedState, createChatState, type ChatState } from "./chat-state";
|
||||
import { codexPanelDisplayTitle, getThreadTitle, inheritedForkThreadName, upsertThread } from "../../domain/threads/model";
|
||||
import { exportArchivedThreadMarkdown } from "../../domain/threads/export";
|
||||
import {
|
||||
|
|
@ -68,28 +68,28 @@ import {
|
|||
} from "../../domain/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";
|
||||
import { PanelMessageRenderer } from "./message-renderer";
|
||||
import type { ChatTurnDiffViewState } from "./ui/turn-diff";
|
||||
import { ChatMessageRenderer } from "./chat-message-renderer";
|
||||
|
||||
export interface CodexPanelHost {
|
||||
export interface CodexChatHost {
|
||||
readonly settings: CodexPanelSettings;
|
||||
readonly vaultPath: string;
|
||||
openThreadInNewView(threadId: string): Promise<unknown>;
|
||||
openTurnDiff(state: TurnDiffViewState): Promise<void>;
|
||||
openTurnDiff(state: ChatTurnDiffViewState): Promise<void>;
|
||||
refreshOpenThreadLists(): void;
|
||||
}
|
||||
|
||||
export class CodexPanelView extends ItemView {
|
||||
export class CodexChatView extends ItemView {
|
||||
private client: AppServerClient | null = null;
|
||||
private readonly connection: ConnectionManager;
|
||||
private readonly controller: PanelController;
|
||||
private readonly session: PanelSessionController;
|
||||
private readonly controller: ChatController;
|
||||
private readonly session: ChatSessionController;
|
||||
private readonly history: ThreadHistoryLoader;
|
||||
private readonly threadRename: ThreadRenameController;
|
||||
private readonly state: PanelState = createPanelState();
|
||||
private readonly state: ChatState = createChatState();
|
||||
private readonly viewId = `codex-panel-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
private readonly composerController: PanelComposerController;
|
||||
private readonly messageRenderer: PanelMessageRenderer;
|
||||
private readonly composerController: ChatComposerController;
|
||||
private readonly messageRenderer: ChatMessageRenderer;
|
||||
private readonly blockSignatures = new Map<string, string>();
|
||||
private toolbarEl: HTMLElement | null = null;
|
||||
private configSlotEl: HTMLElement | null = null;
|
||||
|
|
@ -101,10 +101,10 @@ export class CodexPanelView extends ItemView {
|
|||
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private readonly plugin: CodexPanelHost,
|
||||
private readonly plugin: CodexChatHost,
|
||||
) {
|
||||
super(leaf);
|
||||
this.messageRenderer = new PanelMessageRenderer({
|
||||
this.messageRenderer = new ChatMessageRenderer({
|
||||
app: this.app,
|
||||
owner: this,
|
||||
state: this.state,
|
||||
|
|
@ -122,7 +122,7 @@ export class CodexPanelView extends ItemView {
|
|||
pendingRequestsSignature: () => this.pendingRequestsSignature(),
|
||||
renderPendingRequests: () => this.createPendingRequestsElement(),
|
||||
});
|
||||
this.composerController = new PanelComposerController({
|
||||
this.composerController = new ChatComposerController({
|
||||
app: this.app,
|
||||
state: this.state,
|
||||
viewId: this.viewId,
|
||||
|
|
@ -156,7 +156,7 @@ export class CodexPanelView extends ItemView {
|
|||
this.render();
|
||||
},
|
||||
});
|
||||
this.controller = new PanelController(this.state, {
|
||||
this.controller = new ChatController(this.state, {
|
||||
refreshThreads: () => void this.refreshThreads(),
|
||||
refreshSkills: (forceReload) => void this.refreshSkills(forceReload),
|
||||
maybeNameThread: (threadId, turn) => {
|
||||
|
|
@ -169,7 +169,7 @@ export class CodexPanelView extends ItemView {
|
|||
respondToServerRequest: (requestId, result) => this.respondToServerRequest(requestId, result),
|
||||
rejectServerRequest: (requestId, code, message) => this.rejectServerRequest(requestId, code, message),
|
||||
});
|
||||
this.session = new PanelSessionController({
|
||||
this.session = new ChatSessionController({
|
||||
state: this.state,
|
||||
vaultPath: this.plugin.vaultPath,
|
||||
currentClient: () => this.connection.currentClient(),
|
||||
|
|
@ -710,7 +710,7 @@ export class CodexPanelView extends ItemView {
|
|||
return latestProposedPlanItem(this.state.displayItems)?.id === item.id;
|
||||
}
|
||||
|
||||
private toggleRuntimePicker(picker: NonNullable<PanelState["runtimePicker"]>): void {
|
||||
private toggleRuntimePicker(picker: NonNullable<ChatState["runtimePicker"]>): void {
|
||||
this.state.runtimePicker = this.state.runtimePicker === picker ? null : picker;
|
||||
if (this.state.runtimePicker !== null) {
|
||||
this.state.openDetails.delete("history");
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { MarkdownView, Notice, type Editor, type Plugin } from "obsidian";
|
||||
|
||||
import type { RewriteRuntimeSettings, RewriteSession } from "./model";
|
||||
import { RewriteSelectionPopover } from "./popover";
|
||||
import type { SelectionRewriteRuntimeSettings, SelectionRewriteSession } from "./model";
|
||||
import { SelectionRewritePopover } from "./popover";
|
||||
import type { SendShortcut } from "../../settings/model";
|
||||
|
||||
export interface RewriteSelectionCommandHost extends Plugin {
|
||||
export interface SelectionRewriteCommandHost extends Plugin {
|
||||
settings: {
|
||||
codexPath: string;
|
||||
sendShortcut: SendShortcut;
|
||||
} & RewriteRuntimeSettings;
|
||||
} & SelectionRewriteRuntimeSettings;
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandHost): void {
|
||||
const activePopovers = new Set<RewriteSelectionPopover>();
|
||||
export function registerSelectionRewriteCommand(plugin: SelectionRewriteCommandHost): void {
|
||||
const activePopovers = new Set<SelectionRewritePopover>();
|
||||
|
||||
plugin.register(() => {
|
||||
for (const popover of activePopovers) popover.close();
|
||||
|
|
@ -35,7 +35,7 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH
|
|||
return;
|
||||
}
|
||||
|
||||
const session: RewriteSession = {
|
||||
const session: SelectionRewriteSession = {
|
||||
filePath: view.file.path,
|
||||
targetRange: {
|
||||
from: clonePosition(editor.getCursor("from")),
|
||||
|
|
@ -50,7 +50,7 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH
|
|||
debugText: null,
|
||||
};
|
||||
|
||||
const popover = new RewriteSelectionPopover({
|
||||
const popover = new SelectionRewritePopover({
|
||||
codexPath: plugin.settings.codexPath,
|
||||
cwd: plugin.vaultPath,
|
||||
editor,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { isComposerSendKey, type ComposerSendKeyEvent } from "../chat/composer/keys";
|
||||
import type { SendShortcut } from "../../settings/model";
|
||||
|
||||
export type RewriteGenerateKeyEvent = ComposerSendKeyEvent;
|
||||
export type SelectionRewriteGenerateKeyEvent = ComposerSendKeyEvent;
|
||||
|
||||
export function isRewriteGenerateKey(event: RewriteGenerateKeyEvent, shortcut: SendShortcut): boolean {
|
||||
export function isSelectionRewriteGenerateKey(event: SelectionRewriteGenerateKeyEvent, shortcut: SendShortcut): boolean {
|
||||
return isComposerSendKey(event, shortcut);
|
||||
}
|
||||
|
||||
export function isRewriteActionKey(event: RewriteGenerateKeyEvent): boolean {
|
||||
export function isSelectionRewriteActionKey(event: SelectionRewriteGenerateKeyEvent): boolean {
|
||||
if (event.isComposing || event.key !== "Enter" || event.shiftKey || event.altKey) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { EditorPosition } from "obsidian";
|
||||
import type { ReasoningEffort } from "../../generated/app-server/ReasoningEffort";
|
||||
|
||||
export type RewriteStatus = "editing-prompt" | "generating" | "preview" | "applied" | "cancelled" | "failed";
|
||||
export type SelectionRewriteStatus = "editing-prompt" | "generating" | "preview" | "applied" | "cancelled" | "failed";
|
||||
|
||||
export interface RewriteSession {
|
||||
export interface SelectionRewriteSession {
|
||||
filePath: string;
|
||||
targetRange: {
|
||||
from: EditorPosition;
|
||||
|
|
@ -12,17 +12,17 @@ export interface RewriteSession {
|
|||
originalText: string;
|
||||
noteText: string;
|
||||
instruction: string;
|
||||
status: RewriteStatus;
|
||||
status: SelectionRewriteStatus;
|
||||
streamText: string;
|
||||
replacementText: string | null;
|
||||
debugText: string | null;
|
||||
}
|
||||
|
||||
export interface RewriteRuntimeSettings {
|
||||
export interface SelectionRewriteRuntimeSettings {
|
||||
rewriteSelectionModel: string | null;
|
||||
rewriteSelectionEffort: ReasoningEffort | null;
|
||||
}
|
||||
|
||||
export function canApplyRewrite(currentText: string, originalText: string): boolean {
|
||||
export function canApplySelectionRewrite(currentText: string, originalText: string): boolean {
|
||||
return currentText === originalText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
import type { Turn } from "../../generated/app-server/v2/Turn";
|
||||
|
||||
export interface RewriteOutput {
|
||||
export interface SelectionRewriteOutput {
|
||||
replacementText: string;
|
||||
}
|
||||
|
||||
export interface RewriteOutputParseResult {
|
||||
output: RewriteOutput | null;
|
||||
export interface SelectionRewriteOutputParseResult {
|
||||
output: SelectionRewriteOutput | null;
|
||||
rawText: string | null;
|
||||
}
|
||||
|
||||
export class RewriteOutputError extends Error {
|
||||
export class SelectionRewriteOutputError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly rawText: string | null,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "RewriteOutputError";
|
||||
this.name = "SelectionRewriteOutputError";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRewriteOutput(text: string): RewriteOutput | null {
|
||||
export function parseSelectionRewriteOutput(text: string): SelectionRewriteOutput | null {
|
||||
try {
|
||||
const parsed = JSON.parse(text.trim()) as unknown;
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
|
|
@ -31,14 +31,14 @@ export function parseRewriteOutput(text: string): RewriteOutput | null {
|
|||
}
|
||||
}
|
||||
|
||||
export function rewriteOutputFromTurn(turn: Turn): RewriteOutput | null {
|
||||
return rewriteOutputParseResultFromTurn(turn).output;
|
||||
export function selectionRewriteOutputFromTurn(turn: Turn): SelectionRewriteOutput | null {
|
||||
return selectionRewriteOutputParseResultFromTurn(turn).output;
|
||||
}
|
||||
|
||||
export function rewriteOutputParseResultFromTurn(turn: Turn): RewriteOutputParseResult {
|
||||
export function selectionRewriteOutputParseResultFromTurn(turn: Turn): SelectionRewriteOutputParseResult {
|
||||
const text = lastAgentMessageText(turn);
|
||||
if (!text) return { output: null, rawText: null };
|
||||
return { output: parseRewriteOutput(text), rawText: text };
|
||||
return { output: parseSelectionRewriteOutput(text), rawText: text };
|
||||
}
|
||||
|
||||
function lastAgentMessageText(turn: Turn): string | null {
|
||||
|
|
|
|||
|
|
@ -4,29 +4,29 @@ import { diffLineClass, displayDiffLineText, displayDiffLines } from "../../shar
|
|||
import { createIconButton } from "../../shared/ui/components";
|
||||
import { syncTextareaHeight } from "../../shared/ui/textarea-autogrow";
|
||||
import { buildSelectionUnifiedDiff } from "./diff";
|
||||
import { isRewriteActionKey, isRewriteGenerateKey } from "./keys";
|
||||
import { canApplyRewrite, type RewriteRuntimeSettings, type RewriteSession } from "./model";
|
||||
import { RewriteOutputError } from "./output";
|
||||
import { positionRewritePopover } from "./position";
|
||||
import { buildRewritePrompt } from "./prompt";
|
||||
import { runRewriteSelection, type RewriteActivity } from "./runner";
|
||||
import { isSelectionRewriteActionKey, isSelectionRewriteGenerateKey } from "./keys";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteRuntimeSettings, type SelectionRewriteSession } from "./model";
|
||||
import { SelectionRewriteOutputError } from "./output";
|
||||
import { positionSelectionRewritePopover } from "./position";
|
||||
import { buildSelectionRewritePrompt } from "./prompt";
|
||||
import { runSelectionRewrite, type SelectionRewriteActivity } from "./runner";
|
||||
import type { SendShortcut } from "../../settings/model";
|
||||
|
||||
const POPOVER_MARGIN = 8;
|
||||
|
||||
export interface RewriteSelectionPopoverOptions {
|
||||
export interface SelectionRewritePopoverOptions {
|
||||
codexPath: string;
|
||||
cwd: string;
|
||||
editor: Editor;
|
||||
onClose?: () => void;
|
||||
runtimeSettings: RewriteRuntimeSettings;
|
||||
runtimeSettings: SelectionRewriteRuntimeSettings;
|
||||
sendShortcut: SendShortcut;
|
||||
session: RewriteSession;
|
||||
session: SelectionRewriteSession;
|
||||
}
|
||||
|
||||
type Cleanup = () => void;
|
||||
|
||||
interface RewritePopoverElements {
|
||||
interface SelectionRewriteElements {
|
||||
root: HTMLElement;
|
||||
instruction: HTMLTextAreaElement;
|
||||
generateButton: HTMLButtonElement;
|
||||
|
|
@ -38,12 +38,12 @@ interface RewritePopoverElements {
|
|||
debug: HTMLDetailsElement | null;
|
||||
}
|
||||
|
||||
export class RewriteSelectionPopover {
|
||||
private elements: RewritePopoverElements | null = null;
|
||||
export class SelectionRewritePopover {
|
||||
private elements: SelectionRewriteElements | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
private readonly cleanups: Cleanup[] = [];
|
||||
|
||||
constructor(private readonly options: RewriteSelectionPopoverOptions) {}
|
||||
constructor(private readonly options: SelectionRewritePopoverOptions) {}
|
||||
|
||||
open(): void {
|
||||
this.close();
|
||||
|
|
@ -104,10 +104,10 @@ export class RewriteSelectionPopover {
|
|||
this.abortController = abortController;
|
||||
|
||||
try {
|
||||
const output = await runRewriteSelection({
|
||||
const output = await runSelectionRewrite({
|
||||
codexPath: this.options.codexPath,
|
||||
cwd: this.options.cwd,
|
||||
prompt: buildRewritePrompt(this.options.session),
|
||||
prompt: buildSelectionRewritePrompt(this.options.session),
|
||||
runtimeSettings: this.options.runtimeSettings,
|
||||
onActivity: (activity) => {
|
||||
this.updateActivity(activity);
|
||||
|
|
@ -118,7 +118,7 @@ export class RewriteSelectionPopover {
|
|||
signal: abortController.signal,
|
||||
});
|
||||
if (abortController.signal.aborted) return;
|
||||
this.showRewritePreview(output.replacementText);
|
||||
this.showSelectionRewritePreview(output.replacementText);
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
this.options.session.status = "cancelled";
|
||||
|
|
@ -146,17 +146,17 @@ export class RewriteSelectionPopover {
|
|||
this.position();
|
||||
}
|
||||
|
||||
private updateActivity(activity: RewriteActivity): void {
|
||||
private updateActivity(activity: SelectionRewriteActivity): void {
|
||||
this.setStatus(activity === "reasoning" ? "Reasoning" : "Writing replacement", { active: true });
|
||||
}
|
||||
|
||||
private createElements(): RewritePopoverElements {
|
||||
const root = activeDocument.body.createDiv({ cls: "codex-panel-rewrite-popover" });
|
||||
private createElements(): SelectionRewriteElements {
|
||||
const root = activeDocument.body.createDiv({ cls: "codex-panel-selection-rewrite" });
|
||||
root.setAttr("role", "dialog");
|
||||
root.setAttr("aria-label", "Rewrite selection");
|
||||
|
||||
const instruction = root.createEl("textarea", {
|
||||
cls: "codex-panel__input codex-panel-rewrite-popover__instruction",
|
||||
cls: "codex-panel__input codex-panel-selection-rewrite__instruction",
|
||||
attr: { placeholder: "How should Codex rewrite this selection?" },
|
||||
});
|
||||
instruction.value = this.options.session.instruction;
|
||||
|
|
@ -167,42 +167,47 @@ export class RewriteSelectionPopover {
|
|||
};
|
||||
instruction.onkeydown = (event) => {
|
||||
const hasReplacement = this.options.session.replacementText !== null;
|
||||
if (!(hasReplacement ? isRewriteActionKey(event) : isRewriteGenerateKey(event, this.options.sendShortcut))) return;
|
||||
if (!(hasReplacement ? isSelectionRewriteActionKey(event) : isSelectionRewriteGenerateKey(event, this.options.sendShortcut))) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void this.generate();
|
||||
};
|
||||
|
||||
const promptRow = root.createDiv({ cls: "codex-panel-rewrite-popover__prompt-row" });
|
||||
const promptRow = root.createDiv({ cls: "codex-panel-selection-rewrite__prompt-row" });
|
||||
promptRow.append(instruction);
|
||||
const controls = promptRow.createDiv({ cls: "codex-panel__composer-actions codex-panel-rewrite-popover__controls" });
|
||||
const controls = promptRow.createDiv({ cls: "codex-panel__composer-actions codex-panel-selection-rewrite__controls" });
|
||||
const generateButton = createIconButton(
|
||||
controls,
|
||||
"sparkles",
|
||||
"Generate",
|
||||
"codex-panel__composer-action codex-panel-rewrite-popover__icon-button",
|
||||
"codex-panel__composer-action codex-panel-selection-rewrite__icon-button",
|
||||
);
|
||||
generateButton.onclick = () => void this.generate();
|
||||
const cancelButton = createIconButton(controls, "x", "Cancel", "codex-panel__composer-action codex-panel-rewrite-popover__icon-button");
|
||||
const cancelButton = createIconButton(
|
||||
controls,
|
||||
"x",
|
||||
"Cancel",
|
||||
"codex-panel__composer-action codex-panel-selection-rewrite__icon-button",
|
||||
);
|
||||
cancelButton.onclick = () => {
|
||||
this.cancel();
|
||||
};
|
||||
|
||||
const status = root.createDiv({ cls: "codex-panel-rewrite-popover__status" });
|
||||
const streamPreview = root.createEl("pre", { cls: "codex-panel-rewrite-popover__stream-preview is-hidden" });
|
||||
const resultRow = root.createDiv({ cls: "codex-panel-rewrite-popover__result-row" });
|
||||
const diff = resultRow.createDiv({ cls: "codex-panel-rewrite-popover__diff" });
|
||||
const status = root.createDiv({ cls: "codex-panel-selection-rewrite__status" });
|
||||
const streamPreview = root.createEl("pre", { cls: "codex-panel-selection-rewrite__stream-preview is-hidden" });
|
||||
const resultRow = root.createDiv({ cls: "codex-panel-selection-rewrite__result-row" });
|
||||
const diff = resultRow.createDiv({ cls: "codex-panel-selection-rewrite__diff" });
|
||||
const applyButton = createIconButton(
|
||||
resultRow,
|
||||
"check",
|
||||
"Apply",
|
||||
"codex-panel__composer-action codex-panel-rewrite-popover__icon-button mod-cta",
|
||||
"codex-panel__composer-action codex-panel-selection-rewrite__icon-button mod-cta",
|
||||
);
|
||||
applyButton.onclick = () => {
|
||||
this.apply();
|
||||
};
|
||||
applyButton.onkeydown = (event) => {
|
||||
if (!isRewriteActionKey(event)) return;
|
||||
if (!isSelectionRewriteActionKey(event)) return;
|
||||
event.preventDefault();
|
||||
this.apply();
|
||||
};
|
||||
|
|
@ -221,7 +226,7 @@ export class RewriteSelectionPopover {
|
|||
this.renderDebug(null);
|
||||
}
|
||||
|
||||
private showRewritePreview(replacementText: string): void {
|
||||
private showSelectionRewritePreview(replacementText: string): void {
|
||||
this.options.session.replacementText = replacementText;
|
||||
this.options.session.status = "preview";
|
||||
this.options.session.streamText = "";
|
||||
|
|
@ -232,7 +237,7 @@ export class RewriteSelectionPopover {
|
|||
|
||||
private showGenerationFailure(error: unknown): void {
|
||||
this.options.session.status = "failed";
|
||||
this.options.session.debugText = error instanceof RewriteOutputError ? error.rawText : null;
|
||||
this.options.session.debugText = error instanceof SelectionRewriteOutputError ? error.rawText : null;
|
||||
this.options.session.streamText = "";
|
||||
this.renderStreamPreview();
|
||||
this.renderDebug(this.options.session.debugText);
|
||||
|
|
@ -251,7 +256,7 @@ export class RewriteSelectionPopover {
|
|||
const replacement = this.options.session.replacementText;
|
||||
if (replacement === null || !this.elements) return;
|
||||
this.elements.diff.empty();
|
||||
renderRewriteDiff(
|
||||
renderSelectionRewriteDiff(
|
||||
this.elements.diff,
|
||||
buildSelectionUnifiedDiff(this.options.session.filePath, this.options.session.originalText, replacement),
|
||||
);
|
||||
|
|
@ -263,7 +268,7 @@ export class RewriteSelectionPopover {
|
|||
this.elements.debug = null;
|
||||
if (!debugText) return;
|
||||
|
||||
const debugEl = this.elements.root.createEl("details", { cls: "codex-panel-rewrite-popover__debug" });
|
||||
const debugEl = this.elements.root.createEl("details", { cls: "codex-panel-selection-rewrite__debug" });
|
||||
debugEl.createEl("summary", { text: "Debug output" });
|
||||
debugEl.createEl("pre", { text: debugText });
|
||||
this.elements.debug = debugEl;
|
||||
|
|
@ -275,7 +280,7 @@ export class RewriteSelectionPopover {
|
|||
|
||||
const { editor, session } = this.options;
|
||||
const currentText = editor.getRange(session.targetRange.from, session.targetRange.to);
|
||||
if (!canApplyRewrite(currentText, session.originalText)) {
|
||||
if (!canApplySelectionRewrite(currentText, session.originalText)) {
|
||||
new Notice("Selection changed. Generate the rewrite again before applying.");
|
||||
this.setStatus("Selection changed. Generate the rewrite again before applying.");
|
||||
return;
|
||||
|
|
@ -299,7 +304,7 @@ export class RewriteSelectionPopover {
|
|||
if (!text && !options.active) return;
|
||||
status.createSpan({ text });
|
||||
if (!options.active) return;
|
||||
const dots = status.createSpan({ cls: "codex-panel-rewrite-popover__status-dots" });
|
||||
const dots = status.createSpan({ cls: "codex-panel-selection-rewrite__status-dots" });
|
||||
dots.createSpan({ text: "." });
|
||||
dots.createSpan({ text: "." });
|
||||
dots.createSpan({ text: "." });
|
||||
|
|
@ -328,7 +333,7 @@ export class RewriteSelectionPopover {
|
|||
|
||||
private position(): void {
|
||||
if (!this.elements) return;
|
||||
if (!positionRewritePopover(this.elements.root, this.options.editor, POPOVER_MARGIN)) this.close();
|
||||
if (!positionSelectionRewritePopover(this.elements.root, this.options.editor, POPOVER_MARGIN)) this.close();
|
||||
}
|
||||
|
||||
private addDomListener<K extends keyof WindowEventMap>(
|
||||
|
|
@ -356,13 +361,13 @@ export class RewriteSelectionPopover {
|
|||
}
|
||||
}
|
||||
|
||||
function renderRewriteDiff(parent: HTMLElement, diff: string): void {
|
||||
const pre = parent.createEl("pre", { cls: "codex-panel__diff codex-panel-rewrite-popover__diff-body" });
|
||||
function renderSelectionRewriteDiff(parent: HTMLElement, diff: string): void {
|
||||
const pre = parent.createEl("pre", { cls: "codex-panel-diff codex-panel-selection-rewrite__diff-body" });
|
||||
for (const line of displayDiffLines(diff)) {
|
||||
if (line.kind === "file" || line.text.startsWith("@@")) continue;
|
||||
const lineClass = diffLineClass(line);
|
||||
pre.createEl("span", {
|
||||
cls: `codex-panel__diff-line codex-panel__diff-line--${lineClass}`,
|
||||
cls: `codex-panel-diff__line codex-panel-diff__line--${lineClass}`,
|
||||
text: displayDiffLineText(line.text, lineClass),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Editor } from "obsidian";
|
||||
|
||||
export function positionRewritePopover(root: HTMLElement, editor: Editor, margin: number): boolean {
|
||||
export function positionSelectionRewritePopover(root: HTMLElement, editor: Editor, margin: number): boolean {
|
||||
if (!root.isConnected) return false;
|
||||
|
||||
const view = editorViewFromEditor(editor);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { RewriteSession } from "./model";
|
||||
import type { SelectionRewriteSession } from "./model";
|
||||
|
||||
const MAX_NOTE_CONTEXT_CHARS = 20_000;
|
||||
|
||||
export const REWRITE_SERVICE_NAME = "codex-panel-rewrite-selection";
|
||||
export const SELECTION_REWRITE_SERVICE_NAME = "codex-panel-rewrite-selection";
|
||||
|
||||
export const REWRITE_DEVELOPER_INSTRUCTIONS = [
|
||||
export const SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS = [
|
||||
"You rewrite selected Obsidian note text.",
|
||||
"Return only JSON matching the requested schema.",
|
||||
"Do not include Markdown fences, explanations, alternatives, or comments outside JSON.",
|
||||
|
|
@ -12,7 +12,7 @@ export const REWRITE_DEVELOPER_INSTRUCTIONS = [
|
|||
"Preserve Obsidian-specific syntax such as wikilinks, block ids, callouts, frontmatter-like text, and Dataview blocks unless the user's instruction explicitly asks to change them.",
|
||||
].join("\n");
|
||||
|
||||
export function buildRewritePrompt(session: RewriteSession): string {
|
||||
export function buildSelectionRewritePrompt(session: SelectionRewriteSession): string {
|
||||
return [
|
||||
"Rewrite the selected text according to the user's instruction.",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import type { Model } from "../../generated/app-server/v2/Model";
|
|||
import type { ThreadItem } from "../../generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../../generated/app-server/v2/Turn";
|
||||
import { runtimeOverride, validatedRuntimeOverride } from "../../runtime/model";
|
||||
import type { RewriteRuntimeSettings } from "./model";
|
||||
import { REWRITE_DEVELOPER_INSTRUCTIONS, REWRITE_SERVICE_NAME } from "./prompt";
|
||||
import { RewriteOutputError, rewriteOutputParseResultFromTurn, type RewriteOutput } from "./output";
|
||||
import type { SelectionRewriteRuntimeSettings } from "./model";
|
||||
import { SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS, SELECTION_REWRITE_SERVICE_NAME } from "./prompt";
|
||||
import { SelectionRewriteOutputError, selectionRewriteOutputParseResultFromTurn, type SelectionRewriteOutput } from "./output";
|
||||
|
||||
const REWRITE_TIMEOUT_MS = 120_000;
|
||||
const SELECTION_REWRITE_TIMEOUT_MS = 120_000;
|
||||
|
||||
const REWRITE_OUTPUT_SCHEMA: JsonValue = {
|
||||
const SELECTION_REWRITE_OUTPUT_SCHEMA: JsonValue = {
|
||||
type: "object",
|
||||
properties: {
|
||||
replacementText: {
|
||||
|
|
@ -23,19 +23,19 @@ const REWRITE_OUTPUT_SCHEMA: JsonValue = {
|
|||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export interface RunRewriteSelectionOptions {
|
||||
export interface RunSelectionRewriteOptions {
|
||||
codexPath: string;
|
||||
cwd: string;
|
||||
prompt: string;
|
||||
runtimeSettings?: RewriteRuntimeSettings;
|
||||
onActivity?: (activity: RewriteActivity) => void;
|
||||
runtimeSettings?: SelectionRewriteRuntimeSettings;
|
||||
onActivity?: (activity: SelectionRewriteActivity) => void;
|
||||
onPreview?: (text: string) => void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export type RewriteActivity = "reasoning" | "writing";
|
||||
export type SelectionRewriteActivity = "reasoning" | "writing";
|
||||
|
||||
export async function runRewriteSelection(options: RunRewriteSelectionOptions): Promise<RewriteOutput> {
|
||||
export async function runSelectionRewrite(options: RunSelectionRewriteOptions): Promise<SelectionRewriteOutput> {
|
||||
throwIfAborted(options.signal);
|
||||
let threadId: string | null = null;
|
||||
let expectedTurnId: string | null = null;
|
||||
|
|
@ -52,7 +52,7 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions):
|
|||
if (completed) return;
|
||||
completed = true;
|
||||
reject(new Error("Timed out while rewriting the selection."));
|
||||
}, REWRITE_TIMEOUT_MS);
|
||||
}, SELECTION_REWRITE_TIMEOUT_MS);
|
||||
|
||||
handleNotification = (notification): void => {
|
||||
if (completed) return;
|
||||
|
|
@ -101,22 +101,22 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions):
|
|||
onExit: () => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
rejectCompletedTurn?.(new Error("Codex rewrite app-server exited."));
|
||||
rejectCompletedTurn?.(new Error("Selection rewrite app-server exited."));
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await abortable(client.connect(), options.signal);
|
||||
const runtime = options.runtimeSettings
|
||||
? await abortable(rewriteRuntimeForClient(client, options.runtimeSettings), options.signal)
|
||||
? await abortable(selectionRewriteRuntimeForClient(client, options.runtimeSettings), options.signal)
|
||||
: {};
|
||||
const threadResponse = await abortable(
|
||||
client.startEphemeralThread(options.cwd, REWRITE_SERVICE_NAME, REWRITE_DEVELOPER_INSTRUCTIONS),
|
||||
client.startEphemeralThread(options.cwd, SELECTION_REWRITE_SERVICE_NAME, SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS),
|
||||
options.signal,
|
||||
);
|
||||
threadId = threadResponse.thread.id;
|
||||
const turnResponse = await abortable(
|
||||
client.startStructuredTurn(threadId, options.cwd, options.prompt, REWRITE_OUTPUT_SCHEMA, runtime.model, runtime.effort),
|
||||
client.startStructuredTurn(threadId, options.cwd, options.prompt, SELECTION_REWRITE_OUTPUT_SCHEMA, runtime.model, runtime.effort),
|
||||
options.signal,
|
||||
);
|
||||
expectedTurnId = turnResponse.turn.id;
|
||||
|
|
@ -124,8 +124,8 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions):
|
|||
turnResponse.turn.status === "completed"
|
||||
? turnWithCollectedItems(turnResponse.turn, completedItems)
|
||||
: await abortable(completedTurn, options.signal);
|
||||
const { output, rawText } = rewriteOutputParseResultFromTurn(turn);
|
||||
if (!output) throw new RewriteOutputError("Codex did not return a valid rewrite patch.", rawText);
|
||||
const { output, rawText } = selectionRewriteOutputParseResultFromTurn(turn);
|
||||
if (!output) throw new SelectionRewriteOutputError("Codex did not return a valid selection rewrite response.", rawText);
|
||||
return output;
|
||||
} finally {
|
||||
completed = true;
|
||||
|
|
@ -135,7 +135,7 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions):
|
|||
}
|
||||
|
||||
function throwIfAborted(signal: AbortSignal | undefined): void {
|
||||
if (signal?.aborted) throw rewriteAbortError();
|
||||
if (signal?.aborted) throw selectionRewriteAbortError();
|
||||
}
|
||||
|
||||
function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
||||
|
|
@ -143,7 +143,7 @@ function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Pro
|
|||
throwIfAborted(signal);
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const onAbort = (): void => {
|
||||
reject(rewriteAbortError());
|
||||
reject(selectionRewriteAbortError());
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
promise.then(resolve, reject).finally(() => {
|
||||
|
|
@ -152,7 +152,7 @@ function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Pro
|
|||
});
|
||||
}
|
||||
|
||||
function rewriteAbortError(): Error {
|
||||
function selectionRewriteAbortError(): Error {
|
||||
return new Error("Selection rewrite cancelled.");
|
||||
}
|
||||
|
||||
|
|
@ -161,25 +161,28 @@ function turnWithCollectedItems(turn: Turn, completedItems: ThreadItem[]): Turn
|
|||
return { ...turn, items: completedItems };
|
||||
}
|
||||
|
||||
export interface RewriteRuntime {
|
||||
export interface SelectionRewriteRuntime {
|
||||
model?: string;
|
||||
effort?: ReasoningEffort;
|
||||
}
|
||||
|
||||
export function rewriteRuntime(settings: RewriteRuntimeSettings): RewriteRuntime {
|
||||
export function selectionRewriteRuntime(settings: SelectionRewriteRuntimeSettings): SelectionRewriteRuntime {
|
||||
return runtimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort });
|
||||
}
|
||||
|
||||
export function validatedRewriteRuntime(settings: RewriteRuntimeSettings, models: Model[]): RewriteRuntime {
|
||||
export function validatedSelectionRewriteRuntime(settings: SelectionRewriteRuntimeSettings, models: Model[]): SelectionRewriteRuntime {
|
||||
return validatedRuntimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort }, models);
|
||||
}
|
||||
|
||||
async function rewriteRuntimeForClient(client: AppServerClient, settings: RewriteRuntimeSettings): Promise<RewriteRuntime> {
|
||||
const runtime = rewriteRuntime(settings);
|
||||
async function selectionRewriteRuntimeForClient(
|
||||
client: AppServerClient,
|
||||
settings: SelectionRewriteRuntimeSettings,
|
||||
): Promise<SelectionRewriteRuntime> {
|
||||
const runtime = selectionRewriteRuntime(settings);
|
||||
if (!runtime.model || !runtime.effort) return runtime;
|
||||
try {
|
||||
const response = await client.listModels(false);
|
||||
return validatedRewriteRuntime(settings, response.data);
|
||||
return validatedSelectionRewriteRuntime(settings, response.data);
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
|
|
|
|||
34
src/main.ts
34
src/main.ts
|
|
@ -1,12 +1,12 @@
|
|||
import { Plugin, type WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants";
|
||||
import { registerRewriteSelectionCommand } from "./features/selection-rewrite/command";
|
||||
import { CodexPanelView } from "./features/chat/view";
|
||||
import { CodexTurnDiffView } from "./features/chat/turn-diff-view";
|
||||
import { registerSelectionRewriteCommand } from "./features/selection-rewrite/command";
|
||||
import { CodexChatView } from "./features/chat/view";
|
||||
import { CodexChatTurnDiffView } from "./features/chat/chat-turn-diff-view";
|
||||
import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData, type CodexPanelSettings } from "./settings/model";
|
||||
import { CodexPanelSettingTab } from "./settings/tab";
|
||||
import { persistedTurnDiffViewState, type TurnDiffViewState } from "./features/chat/ui/turn-diff";
|
||||
import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/ui/turn-diff";
|
||||
|
||||
export default class CodexPanelPlugin extends Plugin {
|
||||
settings: CodexPanelSettings = DEFAULT_SETTINGS;
|
||||
|
|
@ -16,8 +16,8 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
this.vaultPath = getVaultPath(this.app);
|
||||
await this.loadSettings();
|
||||
|
||||
this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexPanelView(leaf, this));
|
||||
this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexTurnDiffView(leaf));
|
||||
this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexChatView(leaf, this));
|
||||
this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexChatTurnDiffView(leaf));
|
||||
|
||||
this.addRibbonIcon("bot-message-square", "Open panel", () => {
|
||||
void this.activateView();
|
||||
|
|
@ -44,40 +44,40 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
registerRewriteSelectionCommand(this);
|
||||
registerSelectionRewriteCommand(this);
|
||||
|
||||
this.addSettingTab(new CodexPanelSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
async activateView(): Promise<CodexPanelView> {
|
||||
async activateView(): Promise<CodexChatView> {
|
||||
const leaf = await this.app.workspace.ensureSideLeaf(VIEW_TYPE_CODEX_PANEL, "right", {
|
||||
active: true,
|
||||
reveal: true,
|
||||
});
|
||||
return leaf.view as CodexPanelView;
|
||||
return leaf.view as CodexChatView;
|
||||
}
|
||||
|
||||
async activateNewView(): Promise<CodexPanelView> {
|
||||
async activateNewView(): Promise<CodexChatView> {
|
||||
const leaf = this.createRightSidebarTab();
|
||||
if (!leaf) throw new Error("Could not create a right sidebar leaf.");
|
||||
|
||||
await leaf.setViewState({ type: VIEW_TYPE_CODEX_PANEL, active: true });
|
||||
await this.app.workspace.revealLeaf(leaf);
|
||||
return leaf.view as CodexPanelView;
|
||||
return leaf.view as CodexChatView;
|
||||
}
|
||||
|
||||
async openThreadInNewView(threadId: string): Promise<CodexPanelView> {
|
||||
async openThreadInNewView(threadId: string): Promise<CodexChatView> {
|
||||
const view = await this.activateNewView();
|
||||
await view.openThread(threadId);
|
||||
return view;
|
||||
}
|
||||
|
||||
async openTurnDiff(state: TurnDiffViewState): Promise<void> {
|
||||
async openTurnDiff(state: ChatTurnDiffViewState): Promise<void> {
|
||||
const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_TURN_DIFF).at(0);
|
||||
const leaf = existing ?? this.app.workspace.getLeaf("tab");
|
||||
await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedTurnDiffViewState(state) } });
|
||||
await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedChatTurnDiffViewState(state) } });
|
||||
await leaf.loadIfDeferred();
|
||||
if (leaf.view instanceof CodexTurnDiffView) {
|
||||
if (leaf.view instanceof CodexChatTurnDiffView) {
|
||||
leaf.view.setDiffPayload(state);
|
||||
}
|
||||
await this.app.workspace.revealLeaf(leaf);
|
||||
|
|
@ -85,7 +85,7 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
|
||||
refreshOpenViews(): void {
|
||||
for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)) {
|
||||
if (leaf.view instanceof CodexPanelView) {
|
||||
if (leaf.view instanceof CodexChatView) {
|
||||
leaf.view.refreshSettings();
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
|
||||
refreshOpenThreadLists(): void {
|
||||
for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)) {
|
||||
if (leaf.view instanceof CodexPanelView) {
|
||||
if (leaf.view instanceof CodexChatView) {
|
||||
leaf.view.refreshThreadList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
90
styles.css
90
styles.css
|
|
@ -1,6 +1,6 @@
|
|||
.codex-panel,
|
||||
.codex-panel-settings,
|
||||
.codex-panel-rewrite-popover {
|
||||
.codex-panel-selection-rewrite {
|
||||
--codex-panel-top-icon-size: var(--icon-s, 16px);
|
||||
--codex-panel-icon-button-inline-size: calc(var(--codex-panel-top-icon-size) + var(--size-2-3, 6px) * 2);
|
||||
--codex-panel-icon-button-block-size: calc(var(--codex-panel-top-icon-size) + var(--size-2-2, 4px) * 2);
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-leaf-content[data-type="codex-app-server-panel"] .view-content.codex-panel {
|
||||
.workspace-leaf-content[data-type="codex-panel-view"] .view-content.codex-panel {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -696,7 +696,7 @@
|
|||
.codex-panel__config-title,
|
||||
.codex-panel__approval-details summary,
|
||||
.codex-panel__output summary,
|
||||
.codex-panel__diff-file summary,
|
||||
.codex-panel-diff-file summary,
|
||||
.codex-panel__output-title {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
|
|
@ -704,7 +704,7 @@
|
|||
|
||||
.codex-panel__approval-details summary:hover,
|
||||
.codex-panel__output summary:hover,
|
||||
.codex-panel__diff-file summary:hover {
|
||||
.codex-panel-diff-file summary:hover {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
|
|
@ -1309,10 +1309,10 @@
|
|||
}
|
||||
|
||||
.codex-panel__tool-result > .codex-panel__output,
|
||||
.codex-panel__tool-result > .codex-panel__diff-file,
|
||||
.codex-panel__tool-result > .codex-panel-diff-file,
|
||||
.codex-panel__tool-result > .codex-panel__meta-grid,
|
||||
.codex-panel__tool-result-details > .codex-panel__output,
|
||||
.codex-panel__tool-result-details > .codex-panel__diff-file,
|
||||
.codex-panel__tool-result-details > .codex-panel-diff-file,
|
||||
.codex-panel__tool-result-details > .codex-panel__meta-grid {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
}
|
||||
|
||||
.codex-panel__output,
|
||||
.codex-panel__diff-file {
|
||||
.codex-panel-diff-file {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -1404,7 +1404,7 @@
|
|||
}
|
||||
|
||||
.codex-panel__output pre,
|
||||
.codex-panel__diff {
|
||||
.codex-panel-diff {
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
margin: 6px 0 0;
|
||||
|
|
@ -1422,41 +1422,41 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.codex-panel__diff {
|
||||
.codex-panel-diff {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.codex-panel__diff-line {
|
||||
.codex-panel-diff__line {
|
||||
display: block;
|
||||
border-left: 2px solid transparent;
|
||||
padding-left: var(--size-2-3, 6px);
|
||||
min-height: 1.35em;
|
||||
}
|
||||
|
||||
.codex-panel__diff-line--added {
|
||||
.codex-panel-diff__line--added {
|
||||
border-left-color: var(--text-success);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.codex-panel__diff-line--removed {
|
||||
.codex-panel-diff__line--removed {
|
||||
border-left-color: var(--text-error);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.codex-panel__diff-line--hunk {
|
||||
.codex-panel-diff__line--hunk {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.codex-panel__diff-line--file {
|
||||
.codex-panel-diff__line--file {
|
||||
color: var(--text-normal);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.codex-panel__diff-line--context {
|
||||
.codex-panel-diff__line--context {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff {
|
||||
.codex-panel-chat-turn-diff {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-3, 12px);
|
||||
|
|
@ -1465,51 +1465,51 @@
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__header {
|
||||
.codex-panel-chat-turn-diff__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--size-4-2, 8px);
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__title-block {
|
||||
.codex-panel-chat-turn-diff__title-block {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__title {
|
||||
.codex-panel-chat-turn-diff__title {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__meta,
|
||||
.codex-panel-turn-diff__files,
|
||||
.codex-panel-turn-diff__empty {
|
||||
.codex-panel-chat-turn-diff__meta,
|
||||
.codex-panel-chat-turn-diff__files,
|
||||
.codex-panel-chat-turn-diff__empty {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__copy {
|
||||
.codex-panel-chat-turn-diff__copy {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__files {
|
||||
.codex-panel-chat-turn-diff__files {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__files ul {
|
||||
.codex-panel-chat-turn-diff__files ul {
|
||||
margin-block: var(--size-2-2, 4px) 0;
|
||||
}
|
||||
|
||||
.codex-panel-turn-diff__diff.codex-panel__diff {
|
||||
.codex-panel-chat-turn-diff__diff.codex-panel-diff {
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover {
|
||||
.codex-panel-selection-rewrite {
|
||||
position: fixed;
|
||||
z-index: var(--layer-popover, 30);
|
||||
box-sizing: border-box;
|
||||
|
|
@ -1527,59 +1527,59 @@
|
|||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__prompt-row {
|
||||
.codex-panel-selection-rewrite__prompt-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--codex-panel-section-gap);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__result-row {
|
||||
.codex-panel-selection-rewrite__result-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--codex-panel-section-gap);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__result-row.is-hidden,
|
||||
.codex-panel-rewrite-popover__diff:empty {
|
||||
.codex-panel-selection-rewrite__result-row.is-hidden,
|
||||
.codex-panel-selection-rewrite__diff:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__icon-button.is-hidden {
|
||||
.codex-panel-selection-rewrite__icon-button.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status {
|
||||
.codex-panel-selection-rewrite__status {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status:empty {
|
||||
.codex-panel-selection-rewrite__status:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status.is-active {
|
||||
.codex-panel-selection-rewrite__status.is-active {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status-dots span {
|
||||
.codex-panel-selection-rewrite__status-dots span {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status.is-active .codex-panel-rewrite-popover__status-dots span {
|
||||
.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span {
|
||||
animation: codex-panel-reasoning-dot 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status.is-active .codex-panel-rewrite-popover__status-dots span:nth-child(2) {
|
||||
.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span:nth-child(2) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__status.is-active .codex-panel-rewrite-popover__status-dots span:nth-child(3) {
|
||||
.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span:nth-child(3) {
|
||||
animation-delay: 0.36s;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__stream-preview {
|
||||
.codex-panel-selection-rewrite__stream-preview {
|
||||
box-sizing: border-box;
|
||||
max-height: min(180px, 28vh);
|
||||
margin: 0;
|
||||
|
|
@ -1592,11 +1592,11 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__stream-preview.is-hidden {
|
||||
.codex-panel-selection-rewrite__stream-preview.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__diff-body {
|
||||
.codex-panel-selection-rewrite__diff-body {
|
||||
box-sizing: border-box;
|
||||
max-height: min(300px, 38vh);
|
||||
width: 100%;
|
||||
|
|
@ -1606,16 +1606,16 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__debug {
|
||||
.codex-panel-selection-rewrite__debug {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__debug.is-hidden {
|
||||
.codex-panel-selection-rewrite__debug.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__debug pre {
|
||||
.codex-panel-selection-rewrite__debug pre {
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
margin: var(--size-2-3, 6px) 0 0;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadItem } from "../../src/generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../../src/generated/app-server/v2/Turn";
|
||||
import type { Thread } from "../../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadItem } from "../../../src/generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../../../src/generated/app-server/v2/Turn";
|
||||
import {
|
||||
exportArchivedThreadMarkdown,
|
||||
markdownFromThread,
|
||||
normalizedArchiveTags,
|
||||
type ArchiveExportAdapter,
|
||||
} from "../../src/domain/threads/export";
|
||||
import { referencedThreadPrompt } from "../../src/domain/threads/reference";
|
||||
} from "../../../src/domain/threads/export";
|
||||
import { referencedThreadPrompt } from "../../../src/domain/threads/reference";
|
||||
|
||||
describe("thread archive export", () => {
|
||||
it("writes frontmatter and readable user/codex turns with turn timestamps", () => {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import type { Turn } from "../../src/generated/app-server/v2/Turn";
|
||||
import { referencedThreadDisplayFromPrompt, referencedThreadPrompt, referencedThreadTurns } from "../../src/domain/threads/reference";
|
||||
import type { Thread } from "../../../src/generated/app-server/v2/Thread";
|
||||
import type { Turn } from "../../../src/generated/app-server/v2/Turn";
|
||||
import { referencedThreadDisplayFromPrompt, referencedThreadPrompt, referencedThreadTurns } from "../../../src/domain/threads/reference";
|
||||
|
||||
function thread(overrides: Partial<Thread> = {}): Thread {
|
||||
return {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import { codexPanelDisplayTitle, inheritedForkThreadName, upsertThread } from "../../src/domain/threads/model";
|
||||
import type { Thread } from "../../../src/generated/app-server/v2/Thread";
|
||||
import { codexPanelDisplayTitle, inheritedForkThreadName, upsertThread } from "../../../src/domain/threads/model";
|
||||
|
||||
describe("thread helpers", () => {
|
||||
it("formats Codex panel display titles from the active thread", () => {
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildSelectionUnifiedDiff } from "../../src/features/selection-rewrite/diff";
|
||||
import { isRewriteActionKey, isRewriteGenerateKey, type RewriteGenerateKeyEvent } from "../../src/features/selection-rewrite/keys";
|
||||
import { canApplyRewrite, type RewriteSession } from "../../src/features/selection-rewrite/model";
|
||||
import { parseRewriteOutput, rewriteOutputFromTurn, rewriteOutputParseResultFromTurn } from "../../src/features/selection-rewrite/output";
|
||||
import { buildRewritePrompt } from "../../src/features/selection-rewrite/prompt";
|
||||
import { rewriteRuntime, validatedRewriteRuntime } from "../../src/features/selection-rewrite/runner";
|
||||
import type { Model } from "../../src/generated/app-server/v2/Model";
|
||||
import type { Turn } from "../../src/generated/app-server/v2/Turn";
|
||||
|
||||
describe("editor rewrite output", () => {
|
||||
it("parses valid rewrite JSON", () => {
|
||||
expect(parseRewriteOutput('{"replacementText":"rewritten"}')).toEqual({ replacementText: "rewritten" });
|
||||
});
|
||||
|
||||
it("rejects invalid rewrite JSON", () => {
|
||||
expect(parseRewriteOutput("replacementText: rewritten")).toBeNull();
|
||||
expect(parseRewriteOutput('{"replacementText":42}')).toBeNull();
|
||||
expect(parseRewriteOutput('{"text":"rewritten"}')).toBeNull();
|
||||
});
|
||||
|
||||
it("extracts the final rewrite JSON from a completed turn", () => {
|
||||
expect(
|
||||
rewriteOutputFromTurn(
|
||||
turn([
|
||||
{ type: "agentMessage", id: "a1", text: '{"replacementText":"first"}', phase: "final_answer", memoryCitation: null },
|
||||
{ type: "agentMessage", id: "a2", text: '{"replacementText":"final"}', phase: "final_answer", memoryCitation: null },
|
||||
]),
|
||||
),
|
||||
).toEqual({ replacementText: "final" });
|
||||
});
|
||||
|
||||
it("keeps raw model text when rewrite output parsing fails", () => {
|
||||
expect(
|
||||
rewriteOutputParseResultFromTurn(
|
||||
turn([{ type: "agentMessage", id: "a1", text: "replacementText: final", phase: "final_answer", memoryCitation: null }]),
|
||||
),
|
||||
).toEqual({ output: null, rawText: "replacementText: final" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("editor rewrite prompt", () => {
|
||||
it("always includes note context with the selected text", () => {
|
||||
const prompt = buildRewritePrompt(session());
|
||||
|
||||
expect(prompt).toContain("Context mode: Selection + note context");
|
||||
expect(prompt).toContain("Current note context:");
|
||||
expect(prompt).toContain("# Heading");
|
||||
});
|
||||
|
||||
it("uses fences that cannot be closed by note code blocks", () => {
|
||||
const prompt = buildRewritePrompt(
|
||||
session({
|
||||
originalText: "```ts\nconst value = 1;\n```",
|
||||
noteText: "````markdown\n```ts\nconst value = 1;\n```\n````",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(prompt).toContain("````text\n```ts");
|
||||
expect(prompt).toContain("`````text\n````markdown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editor rewrite diff", () => {
|
||||
it("renders replacements in a selection-scoped unified diff", () => {
|
||||
const diff = buildSelectionUnifiedDiff("Note.md", "alpha\nbeta", "alpha\ngamma");
|
||||
|
||||
expect(diff).toContain("diff --git a/Note.md b/Note.md");
|
||||
expect(diff).toContain("@@ -1,2 +1,2 @@");
|
||||
expect(diff).toContain(" alpha");
|
||||
expect(diff).toContain("-beta");
|
||||
expect(diff).toContain("+gamma");
|
||||
});
|
||||
|
||||
it("renders additions and deletions", () => {
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "", "added")).toContain("+added");
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "removed", "")).toContain("-removed");
|
||||
});
|
||||
|
||||
it("renders unchanged text as context", () => {
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "same", "same")).toContain(" same");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editor rewrite apply guard", () => {
|
||||
it("allows apply only when the current range still matches the original text", () => {
|
||||
expect(canApplyRewrite("original", "original")).toBe(true);
|
||||
expect(canApplyRewrite("changed", "original")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editor rewrite runtime", () => {
|
||||
it("uses explicit rewrite runtime settings", () => {
|
||||
expect(rewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" })).toEqual({
|
||||
model: "gpt-5.4-mini",
|
||||
effort: "minimal",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits rewrite runtime overrides that are set to Codex default", () => {
|
||||
expect(rewriteRuntime({ rewriteSelectionModel: null, rewriteSelectionEffort: null })).toEqual({});
|
||||
});
|
||||
|
||||
it("omits an explicit rewrite effort when the selected model does not support it", () => {
|
||||
expect(
|
||||
validatedRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" }, [
|
||||
model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]),
|
||||
]),
|
||||
).toEqual({ model: "gpt-5.4-mini" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("editor rewrite keys", () => {
|
||||
const baseEvent: RewriteGenerateKeyEvent = {
|
||||
key: "Enter",
|
||||
shiftKey: false,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
isComposing: false,
|
||||
};
|
||||
|
||||
it("generates on plain Enter in Enter mode", () => {
|
||||
expect(isRewriteGenerateKey(baseEvent, "enter")).toBe(true);
|
||||
expect(isRewriteGenerateKey({ ...baseEvent, shiftKey: true }, "enter")).toBe(false);
|
||||
expect(isRewriteGenerateKey({ ...baseEvent, metaKey: true }, "enter")).toBe(false);
|
||||
});
|
||||
|
||||
it("generates on Cmd/Ctrl+Enter in mod-enter mode", () => {
|
||||
expect(isRewriteGenerateKey({ ...baseEvent, metaKey: true }, "mod-enter")).toBe(true);
|
||||
expect(isRewriteGenerateKey({ ...baseEvent, ctrlKey: true }, "mod-enter")).toBe(true);
|
||||
expect(isRewriteGenerateKey(baseEvent, "mod-enter")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats plain Enter and Cmd/Ctrl+Enter as preview action keys", () => {
|
||||
expect(isRewriteActionKey(baseEvent)).toBe(true);
|
||||
expect(isRewriteActionKey({ ...baseEvent, metaKey: true })).toBe(true);
|
||||
expect(isRewriteActionKey({ ...baseEvent, ctrlKey: true })).toBe(true);
|
||||
expect(isRewriteActionKey({ ...baseEvent, shiftKey: true })).toBe(false);
|
||||
expect(isRewriteActionKey({ ...baseEvent, isComposing: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function session(overrides: Partial<RewriteSession> = {}): RewriteSession {
|
||||
return {
|
||||
filePath: "Note.md",
|
||||
targetRange: {
|
||||
from: { line: 1, ch: 0 },
|
||||
to: { line: 1, ch: 22 },
|
||||
},
|
||||
originalText: "Rewrite this sentence.",
|
||||
noteText: "# Heading\n\nRewrite this sentence.\n\nNext paragraph.",
|
||||
instruction: "Make it clearer.",
|
||||
status: "editing-prompt",
|
||||
streamText: "",
|
||||
replacementText: null,
|
||||
debugText: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function turn(items: Turn["items"], overrides: Partial<Turn> = {}): Turn {
|
||||
return {
|
||||
id: "turn",
|
||||
items,
|
||||
itemsView: "full",
|
||||
status: "completed",
|
||||
error: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
durationMs: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function model(name: string, efforts: Model["supportedReasoningEfforts"][number]["reasoningEffort"][]): Model {
|
||||
return {
|
||||
id: name,
|
||||
model: name,
|
||||
upgrade: null,
|
||||
upgradeInfo: null,
|
||||
availabilityNux: null,
|
||||
displayName: name,
|
||||
description: "",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: "" })),
|
||||
defaultReasoningEffort: efforts[0] ?? "low",
|
||||
inputModalities: ["text"],
|
||||
supportsPersonality: false,
|
||||
additionalSpeedTiers: [],
|
||||
serviceTiers: [],
|
||||
defaultServiceTier: null,
|
||||
isDefault: false,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { classifyAppServerLog } from "../../src/features/chat/app-server-logs";
|
||||
import { classifyAppServerLog } from "../../../src/features/chat/app-server-logs";
|
||||
|
||||
describe("app-server log classification", () => {
|
||||
it("suppresses raw MCP token refresh stderr", () => {
|
||||
|
|
@ -7,9 +7,9 @@ import {
|
|||
approvalSummary,
|
||||
approvalTitle,
|
||||
toPendingApproval,
|
||||
} from "../../src/features/chat/approvals/model";
|
||||
import type { ServerRequest } from "../../src/generated/app-server/ServerRequest";
|
||||
import type { CommandExecutionApprovalDecision } from "../../src/generated/app-server/v2/CommandExecutionApprovalDecision";
|
||||
} from "../../../../src/features/chat/approvals/model";
|
||||
import type { ServerRequest } from "../../../../src/generated/app-server/ServerRequest";
|
||||
import type { CommandExecutionApprovalDecision } from "../../../../src/generated/app-server/v2/CommandExecutionApprovalDecision";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PanelController } from "../../src/features/chat/controller";
|
||||
import { attachHookRunsToTurn } from "../../src/features/chat/hook-display";
|
||||
import { createPanelState } from "../../src/features/chat/state";
|
||||
import type { ServerNotification } from "../../src/generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../src/generated/app-server/ServerRequest";
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import type { Turn } from "../../src/generated/app-server/v2/Turn";
|
||||
import { ChatController } from "../../../src/features/chat/chat-controller";
|
||||
import { attachHookRunsToTurn } from "../../../src/features/chat/hook-display";
|
||||
import { createChatState } from "../../../src/features/chat/chat-state";
|
||||
import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../../src/generated/app-server/ServerRequest";
|
||||
import type { Thread } from "../../../src/generated/app-server/v2/Thread";
|
||||
import type { Turn } from "../../../src/generated/app-server/v2/Turn";
|
||||
|
||||
function controllerForState(
|
||||
state = createPanelState(),
|
||||
actions: Partial<ConstructorParameters<typeof PanelController>[1]> = {},
|
||||
): PanelController {
|
||||
return new PanelController(state, {
|
||||
state = createChatState(),
|
||||
actions: Partial<ConstructorParameters<typeof ChatController>[1]> = {},
|
||||
): ChatController {
|
||||
return new ChatController(state, {
|
||||
refreshThreads: vi.fn(),
|
||||
refreshSkills: vi.fn(),
|
||||
maybeNameThread: vi.fn(),
|
||||
|
|
@ -28,10 +28,10 @@ function expectPresent<T>(value: T | null | undefined): T {
|
|||
return value;
|
||||
}
|
||||
|
||||
describe("PanelController", () => {
|
||||
describe("ChatController", () => {
|
||||
describe("active turn routing", () => {
|
||||
it("applies matching streaming deltas as assistant markdown", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -45,7 +45,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("marks active reasoning completed when assistant text starts", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
state.busy = true;
|
||||
|
|
@ -66,7 +66,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("streams plan deltas as plain assistant text until completion", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -80,7 +80,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("updates structured turn plan progress", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -110,7 +110,7 @@ describe("PanelController", () => {
|
|||
describe("app-server source of truth updates", () => {
|
||||
it("refreshes skills from disk when app-server reports skill changes", () => {
|
||||
const refreshSkills = vi.fn();
|
||||
const controller = controllerForState(createPanelState(), { refreshSkills });
|
||||
const controller = controllerForState(createChatState(), { refreshSkills });
|
||||
|
||||
controller.handleNotification({
|
||||
method: "skills/changed",
|
||||
|
|
@ -121,7 +121,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("stores the latest aggregated turn diff for the active turn", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -139,7 +139,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("ignores aggregated turn diffs outside the active scope", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -157,7 +157,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("formats hook runs as compact summaries with details", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -210,7 +210,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("omits hook duration details while duration is unavailable", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -253,7 +253,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("attaches unscoped hook runs to the active turn while streaming", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
state.busy = true;
|
||||
|
|
@ -287,7 +287,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("leaves non-prompt unscoped hook runs outside the active turn", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
state.busy = true;
|
||||
|
|
@ -322,7 +322,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("keeps repeated hook runs with the same run id as separate display items", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -356,7 +356,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("attaches pre-turn prompt submit hook runs when the turn starts", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: ["hook-hook-1-1"] };
|
||||
state.displayItems = [
|
||||
|
|
@ -426,7 +426,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("captures only prompt-submit hooks observed during the pending turn start", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: [] };
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -461,7 +461,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("keeps pre-turn prompt submit hooks through turn start and completed-turn reconciliation", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: [] };
|
||||
state.displayItems = [{ id: "local-user-1", kind: "message", role: "user", text: "hello", markdown: true }];
|
||||
|
|
@ -530,7 +530,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("stores account rate limit updates outside thread scope", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const controller = controllerForState(state);
|
||||
|
||||
controller.handleNotification({
|
||||
|
|
@ -555,7 +555,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("records MCP startup status for diagnostics without a chat system message", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const recordMcpStartupStatus = vi.fn();
|
||||
const controller = controllerForState(state, { recordMcpStartupStatus });
|
||||
|
||||
|
|
@ -575,7 +575,7 @@ describe("PanelController", () => {
|
|||
|
||||
describe("interactive server requests", () => {
|
||||
it("queues and resolves requestUserInput server requests", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const respondToServerRequest = vi.fn(() => true);
|
||||
const controller = controllerForState(state, { respondToServerRequest });
|
||||
|
||||
|
|
@ -613,7 +613,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("rejects cancelled requestUserInput server requests", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const rejectServerRequest = vi.fn(() => true);
|
||||
const controller = controllerForState(state, { rejectServerRequest });
|
||||
|
||||
|
|
@ -640,7 +640,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("records manual permission approvals as colored result items", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const respondToServerRequest = vi.fn(() => true);
|
||||
const controller = controllerForState(state, { respondToServerRequest });
|
||||
|
||||
|
|
@ -674,7 +674,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("handles known server request families and rejects unsupported requests by default", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const rejectServerRequest = vi.fn(() => true);
|
||||
const controller = controllerForState(state, { rejectServerRequest });
|
||||
|
||||
|
|
@ -718,7 +718,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("rejects server requests scoped to a different active thread or turn", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const rejectServerRequest = vi.fn(() => true);
|
||||
|
|
@ -762,7 +762,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("keeps pending requests when response delivery fails", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const respondToServerRequest = vi.fn(() => false);
|
||||
const controller = controllerForState(state, { respondToServerRequest });
|
||||
|
||||
|
|
@ -776,7 +776,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("clears pending request state when app-server resolves a request", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.approvals = [
|
||||
{
|
||||
|
|
@ -816,7 +816,7 @@ describe("PanelController", () => {
|
|||
|
||||
describe("thread lifecycle and reconciliation", () => {
|
||||
it("keeps user-visible app-server notices in the message stream", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
const controller = controllerForState(state);
|
||||
|
||||
controller.handleNotification({
|
||||
|
|
@ -833,7 +833,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("clears all active-thread scoped state when the active thread is archived", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
state.activeModel = "gpt-5.5";
|
||||
|
|
@ -898,7 +898,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("does not replace the active cwd from unrelated thread-started notifications", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeThreadCwd = "/workspace/active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -912,7 +912,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("records cwd from active thread-started notifications", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
const controller = controllerForState(state);
|
||||
|
||||
|
|
@ -925,7 +925,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("replaces optimistic user echoes when completed turns are reconciled", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
state.displayItems = [
|
||||
|
|
@ -970,7 +970,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("asks the view to auto-name completed turns", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const maybeNameThread = vi.fn();
|
||||
|
|
@ -998,7 +998,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("updates listed thread names from thread name notifications", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.listedThreads = [thread("thread-active", "/workspace/active")];
|
||||
const refreshThreads = vi.fn();
|
||||
|
|
@ -1014,7 +1014,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("syncs active runtime state from thread settings notifications", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
const controller = controllerForState(state);
|
||||
|
||||
|
|
@ -1050,7 +1050,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("syncs null service tier from settings notifications", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeServiceTier = "standard";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -1114,7 +1114,7 @@ describe("PanelController", () => {
|
|||
];
|
||||
|
||||
for (const { notification, text } of cases) {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
const controller = controllerForState(state);
|
||||
|
||||
|
|
@ -1127,7 +1127,7 @@ describe("PanelController", () => {
|
|||
|
||||
describe("auto-review display", () => {
|
||||
it("renders guardian warnings as review results instead of system messages", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
const controller = controllerForState(state);
|
||||
|
||||
|
|
@ -1147,7 +1147,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("renders auto approval review notifications as upserted review results", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -1194,7 +1194,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("replaces guardian auto-review warnings when structured auto-review notifications arrive", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -1228,7 +1228,7 @@ describe("PanelController", () => {
|
|||
});
|
||||
|
||||
it("ignores guardian auto-review warnings after structured auto-review notifications", () => {
|
||||
const state = createPanelState();
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.activeTurnId = "turn-active";
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isComposerSendKey, type ComposerSendKeyEvent } from "../../src/features/chat/composer/keys";
|
||||
import { isComposerSendKey, type ComposerSendKeyEvent } from "../../../../src/features/chat/composer/keys";
|
||||
|
||||
const baseEvent: ComposerSendKeyEvent = {
|
||||
key: "Enter",
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ReasoningEffort } from "../../src/generated/app-server/ReasoningEffort";
|
||||
import type { Model } from "../../src/generated/app-server/v2/Model";
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import type { ReasoningEffort } from "../../../../src/generated/app-server/ReasoningEffort";
|
||||
import type { Model } from "../../../../src/generated/app-server/v2/Model";
|
||||
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
||||
import {
|
||||
activeComposerSuggestions,
|
||||
applyComposerSuggestionInsertion,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
findWikiLinkSuggestions,
|
||||
nextComposerSuggestionIndex,
|
||||
parseSlashCommand,
|
||||
} from "../../src/features/chat/composer/suggestions";
|
||||
} from "../../../../src/features/chat/composer/suggestions";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { TFile, type App } from "obsidian";
|
||||
|
||||
import { noteCandidates, resolveWikiLinkMention } from "../../src/features/chat/composer/obsidian-context";
|
||||
import { noteCandidates, resolveWikiLinkMention } from "../../../../src/features/chat/composer/obsidian-context";
|
||||
|
||||
describe("Obsidian composer context", () => {
|
||||
it("builds note candidates from markdown files", () => {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { slashCommandHelpLines, slashCommandHelpRows } from "../../src/features/chat/composer/slash-commands";
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import { executeSlashCommand, type SlashCommandExecutionContext } from "../../src/features/chat/slash-commands";
|
||||
import { slashCommandHelpLines, slashCommandHelpRows } from "../../../../src/features/chat/composer/slash-commands";
|
||||
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
||||
import { executeSlashCommand, type SlashCommandExecutionContext } from "../../../../src/features/chat/slash-commands";
|
||||
|
||||
function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCommandExecutionContext {
|
||||
return {
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
parsedWikiLinks,
|
||||
userInputWithWikiLinkMentions,
|
||||
userInputWithWikiLinkMentionsAndSkills,
|
||||
} from "../../src/features/chat/composer/wikilink-context";
|
||||
} from "../../../../src/features/chat/composer/wikilink-context";
|
||||
|
||||
describe("wikilink context", () => {
|
||||
it("parses aliases, subpaths, and duplicate links", () => {
|
||||
|
|
@ -5,8 +5,8 @@ import {
|
|||
capabilityProbeOk,
|
||||
createAppServerDiagnostics,
|
||||
upsertMcpServerDiagnostic,
|
||||
} from "../../src/app-server/compatibility";
|
||||
import { connectionDiagnosticSections, diagnosticAlertLevel } from "../../src/features/chat/diagnostics";
|
||||
} from "../../../src/app-server/compatibility";
|
||||
import { connectionDiagnosticSections, diagnosticAlertLevel } from "../../../src/features/chat/diagnostics";
|
||||
|
||||
describe("connection diagnostics", () => {
|
||||
it("formats base rows, capability probes, and MCP issues for /doctor", () => {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { activeAgentRunSummary } from "../../src/features/chat/display/agent";
|
||||
import { displayBlocksForItems } from "../../src/features/chat/display/blocks";
|
||||
import { activeAgentRunSummary } from "../../../../src/features/chat/display/agent";
|
||||
import { displayBlocksForItems } from "../../../../src/features/chat/display/blocks";
|
||||
import {
|
||||
appendAssistantDelta,
|
||||
appendItemOutput,
|
||||
|
|
@ -9,15 +9,15 @@ import {
|
|||
appendPlanDelta,
|
||||
appendToolOutput,
|
||||
upsertDisplayItem,
|
||||
} from "../../src/features/chat/display/stream-updates";
|
||||
import { normalizeProposedPlanMarkdown, planProgressDisplayItem } from "../../src/features/chat/display/plan";
|
||||
import { pathRelativeToRoot } from "../../src/features/chat/display/paths";
|
||||
import { createAutoReviewResultItem, createReviewResultItem } from "../../src/features/chat/display/review";
|
||||
import { executionState } from "../../src/features/chat/display/state";
|
||||
import { displayItemFromThreadItem, displayItemsFromTurns } from "../../src/features/chat/display/thread-items";
|
||||
import type { DisplayItem } from "../../src/features/chat/display/types";
|
||||
import type { ThreadItem } from "../../src/generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../../src/generated/app-server/v2/Turn";
|
||||
} from "../../../../src/features/chat/display/stream-updates";
|
||||
import { normalizeProposedPlanMarkdown, planProgressDisplayItem } from "../../../../src/features/chat/display/plan";
|
||||
import { pathRelativeToRoot } from "../../../../src/features/chat/display/paths";
|
||||
import { createAutoReviewResultItem, createReviewResultItem } from "../../../../src/features/chat/display/review";
|
||||
import { executionState } from "../../../../src/features/chat/display/state";
|
||||
import { displayItemFromThreadItem, displayItemsFromTurns } from "../../../../src/features/chat/display/thread-items";
|
||||
import type { DisplayItem } from "../../../../src/features/chat/display/types";
|
||||
import type { ThreadItem } from "../../../../src/generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../../../../src/generated/app-server/v2/Turn";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
|
|
@ -6,13 +6,13 @@ import {
|
|||
messageTurnId,
|
||||
routeServerNotification,
|
||||
routeServerRequest,
|
||||
} from "../../src/features/chat/inbound-routing";
|
||||
import type { ServerNotification } from "../../src/generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../src/generated/app-server/ServerRequest";
|
||||
} from "../../../src/features/chat/inbound-routing";
|
||||
import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../../src/generated/app-server/ServerRequest";
|
||||
|
||||
const activeScope = { activeThreadId: "thread-active", activeTurnId: "turn-active" };
|
||||
|
||||
describe("panel inbound routing", () => {
|
||||
describe("chat inbound routing", () => {
|
||||
it("extracts thread and turn ids from app-server messages", () => {
|
||||
const notification = {
|
||||
method: "turn/completed",
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { TFile, type App } from "obsidian";
|
||||
|
||||
import { markdownFileLinkTarget } from "../../src/features/chat/markdown-file-links";
|
||||
import { markdownFileLinkTarget } from "../../../src/features/chat/markdown-file-links";
|
||||
|
||||
describe("markdown file links", () => {
|
||||
it("resolves absolute vault paths", () => {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { McpServerStatus } from "../../src/generated/app-server/v2/McpServerStatus";
|
||||
import { mcpStatusLines } from "../../src/features/chat/mcp-status";
|
||||
import type { McpServerStatus } from "../../../src/generated/app-server/v2/McpServerStatus";
|
||||
import { mcpStatusLines } from "../../../src/features/chat/mcp-status";
|
||||
|
||||
function mcpServer(overrides: Partial<McpServerStatus> = {}): McpServerStatus {
|
||||
return {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../src/features/chat/rollback";
|
||||
import type { DisplayItem } from "../../src/features/chat/display/types";
|
||||
import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../../src/features/chat/rollback";
|
||||
import type { DisplayItem } from "../../../src/features/chat/display/types";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { statusValue, usageLimitStatusLines } from "../../src/features/chat/status-lines";
|
||||
import type { RateLimitSummary } from "../../src/runtime/view";
|
||||
import { statusValue, usageLimitStatusLines } from "../../../src/features/chat/status-lines";
|
||||
import type { RateLimitSummary } from "../../../src/runtime/view";
|
||||
|
||||
describe("status line helpers", () => {
|
||||
it("formats primitive and structured diagnostic values", () => {
|
||||
|
|
@ -10,9 +10,9 @@ import {
|
|||
normalizeGeneratedTitle,
|
||||
titleFromNamingTurn,
|
||||
validatedNamingRuntime,
|
||||
} from "../../src/features/chat/thread-naming";
|
||||
import type { Model } from "../../src/generated/app-server/v2/Model";
|
||||
import type { Turn } from "../../src/generated/app-server/v2/Turn";
|
||||
} from "../../../src/features/chat/thread-naming";
|
||||
import type { Model } from "../../../src/generated/app-server/v2/Model";
|
||||
import type { Turn } from "../../../src/generated/app-server/v2/Turn";
|
||||
|
||||
describe("thread naming", () => {
|
||||
it("extracts the first user request and final assistant response from a completed turn", () => {
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderTextWithWikiLinks, shortSignature } from "../../src/shared/ui/dom";
|
||||
import { renderTextWithWikiLinks, shortSignature } from "../../../../src/shared/ui/dom";
|
||||
import { installObsidianDomShims } from "./dom-test-helpers";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -2,21 +2,21 @@
|
|||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { PendingApproval } from "../../src/features/chat/approvals/model";
|
||||
import type { PendingUserInput } from "../../src/features/chat/user-input/model";
|
||||
import type { PendingApproval } from "../../../../src/features/chat/approvals/model";
|
||||
import type { PendingUserInput } from "../../../../src/features/chat/user-input/model";
|
||||
import {
|
||||
renderComposerShell,
|
||||
renderComposerSuggestions,
|
||||
scrollComposerSuggestionIntoView,
|
||||
syncComposerControls,
|
||||
syncComposerHeight,
|
||||
} from "../../src/features/chat/ui/composer";
|
||||
import { renderPendingRequestMessage } from "../../src/features/chat/ui/pending-request-message";
|
||||
import { renderToolbar, toolbarSignature, type ToolbarViewModel } from "../../src/features/chat/ui/toolbar";
|
||||
import { displayItemSignature } from "../../src/features/chat/display/signature";
|
||||
import { implementPlanCandidateFromState } from "../../src/features/chat/message-renderer";
|
||||
import { messageRenderBlocks as rawMessageRenderBlocks, syncMessageRenderBlocks } from "../../src/features/chat/ui/message-stream";
|
||||
import { displayDiffLines, persistedTurnDiffViewState, renderTurnDiffView } from "../../src/features/chat/ui/turn-diff";
|
||||
} from "../../../../src/features/chat/ui/composer";
|
||||
import { renderPendingRequestMessage } from "../../../../src/features/chat/ui/pending-request-message";
|
||||
import { renderToolbar, toolbarSignature, type ToolbarViewModel } from "../../../../src/features/chat/ui/toolbar";
|
||||
import { displayItemSignature } from "../../../../src/features/chat/display/signature";
|
||||
import { implementPlanCandidateFromState } from "../../../../src/features/chat/chat-message-renderer";
|
||||
import { messageRenderBlocks as rawMessageRenderBlocks, syncMessageRenderBlocks } from "../../../../src/features/chat/ui/message-stream";
|
||||
import { displayDiffLines, persistedChatTurnDiffViewState, renderChatTurnDiffView } from "../../../../src/features/chat/ui/turn-diff";
|
||||
import { composerSuggestionScrollFixture, installObsidianDomShims, topLevelDetailsSummaries } from "./dom-test-helpers";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -950,12 +950,12 @@ describe("message stream block identity and message actions", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("turn diff view decisions", () => {
|
||||
describe("chat turn diff view decisions", () => {
|
||||
it("renders the turn diff view with classified unified diff lines", () => {
|
||||
const parent = document.createElement("div");
|
||||
const copyDiff = vi.fn();
|
||||
|
||||
renderTurnDiffView(
|
||||
renderChatTurnDiffView(
|
||||
parent,
|
||||
{
|
||||
threadId: "019e061e-0046-7653-a362-86de9a47cb5c",
|
||||
|
|
@ -967,26 +967,26 @@ describe("turn diff view decisions", () => {
|
|||
{ copyDiff },
|
||||
);
|
||||
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__title")?.textContent).toBe("Turn diff");
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__meta")?.textContent).toContain("019e061e");
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__files summary")?.textContent).toBe("Changed files");
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__files")?.textContent).toContain("src/main.ts");
|
||||
expect(parent.querySelector(".codex-panel__diff-line--file")?.textContent).toBe("src/main.ts");
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__title")?.textContent).toBe("Turn diff");
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__meta")?.textContent).toContain("019e061e");
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__files summary")?.textContent).toBe("Changed files");
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__files")?.textContent).toContain("src/main.ts");
|
||||
expect(parent.querySelector(".codex-panel-diff__line--file")?.textContent).toBe("src/main.ts");
|
||||
expect(parent.textContent).not.toContain("diff --git");
|
||||
expect(parent.textContent).not.toContain("+++ b/src/main.ts");
|
||||
expect(parent.textContent).not.toContain("-old");
|
||||
expect(parent.textContent).not.toContain("+new");
|
||||
expect(parent.textContent).toContain("old");
|
||||
expect(parent.textContent).toContain("new");
|
||||
expect(parent.querySelectorAll(".codex-panel__diff-line--hunk")).toHaveLength(1);
|
||||
expect(parent.querySelectorAll(".codex-panel__diff-line--removed")).toHaveLength(1);
|
||||
expect(parent.querySelectorAll(".codex-panel__diff-line--added")).toHaveLength(1);
|
||||
parent.querySelector<HTMLButtonElement>(".codex-panel-turn-diff__copy")?.click();
|
||||
expect(parent.querySelectorAll(".codex-panel-diff__line--hunk")).toHaveLength(1);
|
||||
expect(parent.querySelectorAll(".codex-panel-diff__line--removed")).toHaveLength(1);
|
||||
expect(parent.querySelectorAll(".codex-panel-diff__line--added")).toHaveLength(1);
|
||||
parent.querySelector<HTMLButtonElement>(".codex-panel-chat-turn-diff__copy")?.click();
|
||||
expect(copyDiff).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps unified diff text out of persisted turn diff view state", () => {
|
||||
const persisted = persistedTurnDiffViewState({
|
||||
const persisted = persistedChatTurnDiffViewState({
|
||||
threadId: "thread",
|
||||
turnId: "turn",
|
||||
cwd: "/vault/project",
|
||||
|
|
@ -1006,12 +1006,12 @@ describe("turn diff view decisions", () => {
|
|||
it("renders restored turn diff metadata without unavailable diff text", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderTurnDiffView(parent, null, {}, { threadId: "thread", turnId: "turn", cwd: "/vault/project", files: ["src/main.ts"] });
|
||||
renderChatTurnDiffView(parent, null, {}, { threadId: "thread", turnId: "turn", cwd: "/vault/project", files: ["src/main.ts"] });
|
||||
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__meta")?.textContent).toContain("thread / turn");
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__meta")?.textContent).toContain("thread / turn");
|
||||
expect(parent.textContent).toContain("Turn diff is no longer available.");
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__copy")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel-turn-diff__diff")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__copy")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel-chat-turn-diff__diff")).toBeNull();
|
||||
});
|
||||
|
||||
it("simplifies git diff file headers for turn diff display", () => {
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { bottomScrollTop, captureScrollAnchor, isNearScrollBottom, restoreScrollAnchor } from "../../src/features/chat/ui/scroll";
|
||||
import { bottomScrollTop, captureScrollAnchor, isNearScrollBottom, restoreScrollAnchor } from "../../../../src/features/chat/ui/scroll";
|
||||
|
||||
describe("message scroll helpers", () => {
|
||||
it("detects whether the transcript is pinned near the bottom", () => {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { questionDefaultAnswer, toPendingUserInput, userInputResponse } from "../../src/features/chat/user-input/model";
|
||||
import { pendingRequestsSignature } from "../../src/features/chat/request-state";
|
||||
import type { ServerRequest } from "../../src/generated/app-server/ServerRequest";
|
||||
import { questionDefaultAnswer, toPendingUserInput, userInputResponse } from "../../../../src/features/chat/user-input/model";
|
||||
import { pendingRequestsSignature } from "../../../../src/features/chat/request-state";
|
||||
import type { ServerRequest } from "../../../../src/generated/app-server/ServerRequest";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
204
tests/features/selection-rewrite/selection-rewrite.test.ts
Normal file
204
tests/features/selection-rewrite/selection-rewrite.test.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildSelectionUnifiedDiff } from "../../../src/features/selection-rewrite/diff";
|
||||
import {
|
||||
isSelectionRewriteActionKey,
|
||||
isSelectionRewriteGenerateKey,
|
||||
type SelectionRewriteGenerateKeyEvent,
|
||||
} from "../../../src/features/selection-rewrite/keys";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteSession } from "../../../src/features/selection-rewrite/model";
|
||||
import {
|
||||
parseSelectionRewriteOutput,
|
||||
selectionRewriteOutputFromTurn,
|
||||
selectionRewriteOutputParseResultFromTurn,
|
||||
} from "../../../src/features/selection-rewrite/output";
|
||||
import { buildSelectionRewritePrompt } from "../../../src/features/selection-rewrite/prompt";
|
||||
import { selectionRewriteRuntime, validatedSelectionRewriteRuntime } from "../../../src/features/selection-rewrite/runner";
|
||||
import type { Model } from "../../../src/generated/app-server/v2/Model";
|
||||
import type { Turn } from "../../../src/generated/app-server/v2/Turn";
|
||||
|
||||
describe("selection selection rewrite output", () => {
|
||||
it("parses valid selection rewrite JSON", () => {
|
||||
expect(parseSelectionRewriteOutput('{"replacementText":"rewritten"}')).toEqual({ replacementText: "rewritten" });
|
||||
});
|
||||
|
||||
it("rejects invalid selection rewrite JSON", () => {
|
||||
expect(parseSelectionRewriteOutput("replacementText: rewritten")).toBeNull();
|
||||
expect(parseSelectionRewriteOutput('{"replacementText":42}')).toBeNull();
|
||||
expect(parseSelectionRewriteOutput('{"text":"rewritten"}')).toBeNull();
|
||||
});
|
||||
|
||||
it("extracts the final selection rewrite JSON from a completed turn", () => {
|
||||
expect(
|
||||
selectionRewriteOutputFromTurn(
|
||||
turn([
|
||||
{ type: "agentMessage", id: "a1", text: '{"replacementText":"first"}', phase: "final_answer", memoryCitation: null },
|
||||
{ type: "agentMessage", id: "a2", text: '{"replacementText":"final"}', phase: "final_answer", memoryCitation: null },
|
||||
]),
|
||||
),
|
||||
).toEqual({ replacementText: "final" });
|
||||
});
|
||||
|
||||
it("keeps raw model text when selection rewrite output parsing fails", () => {
|
||||
expect(
|
||||
selectionRewriteOutputParseResultFromTurn(
|
||||
turn([{ type: "agentMessage", id: "a1", text: "replacementText: final", phase: "final_answer", memoryCitation: null }]),
|
||||
),
|
||||
).toEqual({ output: null, rawText: "replacementText: final" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection rewrite prompt", () => {
|
||||
it("always includes note context with the selected text", () => {
|
||||
const prompt = buildSelectionRewritePrompt(session());
|
||||
|
||||
expect(prompt).toContain("Context mode: Selection + note context");
|
||||
expect(prompt).toContain("Current note context:");
|
||||
expect(prompt).toContain("# Heading");
|
||||
});
|
||||
|
||||
it("uses fences that cannot be closed by note code blocks", () => {
|
||||
const prompt = buildSelectionRewritePrompt(
|
||||
session({
|
||||
originalText: "```ts\nconst value = 1;\n```",
|
||||
noteText: "````markdown\n```ts\nconst value = 1;\n```\n````",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(prompt).toContain("````text\n```ts");
|
||||
expect(prompt).toContain("`````text\n````markdown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection rewrite diff", () => {
|
||||
it("renders replacements in a selection-scoped unified diff", () => {
|
||||
const diff = buildSelectionUnifiedDiff("Note.md", "alpha\nbeta", "alpha\ngamma");
|
||||
|
||||
expect(diff).toContain("diff --git a/Note.md b/Note.md");
|
||||
expect(diff).toContain("@@ -1,2 +1,2 @@");
|
||||
expect(diff).toContain(" alpha");
|
||||
expect(diff).toContain("-beta");
|
||||
expect(diff).toContain("+gamma");
|
||||
});
|
||||
|
||||
it("renders additions and deletions", () => {
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "", "added")).toContain("+added");
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "removed", "")).toContain("-removed");
|
||||
});
|
||||
|
||||
it("renders unchanged text as context", () => {
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "same", "same")).toContain(" same");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection rewrite apply guard", () => {
|
||||
it("allows apply only when the current range still matches the original text", () => {
|
||||
expect(canApplySelectionRewrite("original", "original")).toBe(true);
|
||||
expect(canApplySelectionRewrite("changed", "original")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection selection rewrite runtime", () => {
|
||||
it("uses explicit selection rewrite runtime settings", () => {
|
||||
expect(selectionRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" })).toEqual({
|
||||
model: "gpt-5.4-mini",
|
||||
effort: "minimal",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits selection rewrite runtime overrides that are set to Codex default", () => {
|
||||
expect(selectionRewriteRuntime({ rewriteSelectionModel: null, rewriteSelectionEffort: null })).toEqual({});
|
||||
});
|
||||
|
||||
it("omits an explicit selection rewrite effort when the selected model does not support it", () => {
|
||||
expect(
|
||||
validatedSelectionRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" }, [
|
||||
model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]),
|
||||
]),
|
||||
).toEqual({ model: "gpt-5.4-mini" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection rewrite keys", () => {
|
||||
const baseEvent: SelectionRewriteGenerateKeyEvent = {
|
||||
key: "Enter",
|
||||
shiftKey: false,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
isComposing: false,
|
||||
};
|
||||
|
||||
it("generates on plain Enter in Enter mode", () => {
|
||||
expect(isSelectionRewriteGenerateKey(baseEvent, "enter")).toBe(true);
|
||||
expect(isSelectionRewriteGenerateKey({ ...baseEvent, shiftKey: true }, "enter")).toBe(false);
|
||||
expect(isSelectionRewriteGenerateKey({ ...baseEvent, metaKey: true }, "enter")).toBe(false);
|
||||
});
|
||||
|
||||
it("generates on Cmd/Ctrl+Enter in mod-enter mode", () => {
|
||||
expect(isSelectionRewriteGenerateKey({ ...baseEvent, metaKey: true }, "mod-enter")).toBe(true);
|
||||
expect(isSelectionRewriteGenerateKey({ ...baseEvent, ctrlKey: true }, "mod-enter")).toBe(true);
|
||||
expect(isSelectionRewriteGenerateKey(baseEvent, "mod-enter")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats plain Enter and Cmd/Ctrl+Enter as preview action keys", () => {
|
||||
expect(isSelectionRewriteActionKey(baseEvent)).toBe(true);
|
||||
expect(isSelectionRewriteActionKey({ ...baseEvent, metaKey: true })).toBe(true);
|
||||
expect(isSelectionRewriteActionKey({ ...baseEvent, ctrlKey: true })).toBe(true);
|
||||
expect(isSelectionRewriteActionKey({ ...baseEvent, shiftKey: true })).toBe(false);
|
||||
expect(isSelectionRewriteActionKey({ ...baseEvent, isComposing: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function session(overrides: Partial<SelectionRewriteSession> = {}): SelectionRewriteSession {
|
||||
return {
|
||||
filePath: "Note.md",
|
||||
targetRange: {
|
||||
from: { line: 1, ch: 0 },
|
||||
to: { line: 1, ch: 22 },
|
||||
},
|
||||
originalText: "Revise this sentence.",
|
||||
noteText: "# Heading\n\nRevise this sentence.\n\nNext paragraph.",
|
||||
instruction: "Make it clearer.",
|
||||
status: "editing-prompt",
|
||||
streamText: "",
|
||||
replacementText: null,
|
||||
debugText: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function turn(items: Turn["items"], overrides: Partial<Turn> = {}): Turn {
|
||||
return {
|
||||
id: "turn",
|
||||
items,
|
||||
itemsView: "full",
|
||||
status: "completed",
|
||||
error: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
durationMs: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function model(name: string, efforts: Model["supportedReasoningEfforts"][number]["reasoningEffort"][]): Model {
|
||||
return {
|
||||
id: name,
|
||||
model: name,
|
||||
upgrade: null,
|
||||
upgradeInfo: null,
|
||||
availabilityNux: null,
|
||||
displayName: name,
|
||||
description: "",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: "" })),
|
||||
defaultReasoningEffort: efforts[0] ?? "low",
|
||||
inputModalities: ["text"],
|
||||
supportsPersonality: false,
|
||||
additionalSpeedTiers: [],
|
||||
serviceTiers: [],
|
||||
defaultServiceTier: null,
|
||||
isDefault: false,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue