mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Organize message stream UI rendering
This commit is contained in:
parent
31e30b637a
commit
552e53681b
45 changed files with 509 additions and 407 deletions
|
|
@ -9,7 +9,7 @@ import type { CodexPanelSettings } from "../../settings/model";
|
|||
import type { RuntimeSnapshot } from "./runtime/snapshot";
|
||||
import type { ChatState, ChatStateStore } from "./state/reducer";
|
||||
import type { ChatTurnDiffViewState } from "./turn-diff/model";
|
||||
import type { ChatMessageScrollIntentController } from "./ui/message-scroll-intent-controller";
|
||||
import type { ChatMessageScrollIntentController } from "./ui/message-stream/scroll-intent-controller";
|
||||
import type { DisplayDetailSection, DisplayItem } from "./display/types";
|
||||
import type { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "./lifecycle";
|
||||
import type { ComposerMetaViewModel } from "./ui/composer";
|
||||
|
|
@ -21,7 +21,7 @@ export interface ChatControllerCompositionPorts {
|
|||
client: ChatPanelClientContext;
|
||||
lifecycle: ChatPanelLifecycleContext;
|
||||
render: ChatPanelRenderContext;
|
||||
messages: ChatPanelMessageContext;
|
||||
messageStream: ChatPanelMessageStreamContext;
|
||||
composerView: ChatPanelComposerContext;
|
||||
runtime: ChatPanelRuntimeContext;
|
||||
thread: ChatThreadContext;
|
||||
|
|
@ -90,13 +90,13 @@ interface ChatPanelRenderContext {
|
|||
panelRoot: () => HTMLElement | null;
|
||||
toolbarNode: () => UiNode;
|
||||
goalNode: () => UiNode;
|
||||
messagesNode: () => UiNode;
|
||||
messageStreamNode: () => UiNode;
|
||||
composerNode: () => UiNode;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
schedule: () => void;
|
||||
}
|
||||
|
||||
interface ChatPanelMessageContext {
|
||||
interface ChatPanelMessageStreamContext {
|
||||
pendingRequestsSignature: () => string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import type { IdentitySync } from "./threads/identity-sync";
|
|||
import type { ResumeController } from "./threads/resume-controller";
|
||||
import type { SelectionActions } from "./threads/selection-actions";
|
||||
import type { ChatViewRenderController } from "./panel/view-render-controller";
|
||||
import type { ChatMessageRenderer } from "./ui/message-stream/renderer";
|
||||
import type { MessageStreamRenderer } from "./ui/message-stream/renderer";
|
||||
import type { ChatControllerCompositionPorts } from "./composition-ports";
|
||||
import { createChatControllerCompositionActions } from "./composition-actions";
|
||||
import {
|
||||
|
|
@ -79,7 +79,7 @@ export interface ChatViewControllers {
|
|||
};
|
||||
render: {
|
||||
controller: ChatViewRenderController;
|
||||
messages: ChatMessageRenderer;
|
||||
messageStream: MessageStreamRenderer;
|
||||
openView: () => void;
|
||||
closeView: () => void;
|
||||
applyViewState: (state: unknown) => void;
|
||||
|
|
@ -102,7 +102,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
panelRoot: ports.render.panelRoot,
|
||||
toolbarNode: ports.render.toolbarNode,
|
||||
goalNode: ports.render.goalNode,
|
||||
messagesNode: ports.render.messagesNode,
|
||||
messageStreamNode: ports.render.messageStreamNode,
|
||||
composerNode: ports.render.composerNode,
|
||||
},
|
||||
});
|
||||
|
|
@ -408,8 +408,8 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
lifecycle: {
|
||||
messageScrollIntent: ports.lifecycle.messageScrollIntent,
|
||||
},
|
||||
messages: {
|
||||
pendingRequestsSignature: ports.messages.pendingRequestsSignature,
|
||||
messageStream: {
|
||||
pendingRequestsSignature: ports.messageStream.pendingRequestsSignature,
|
||||
},
|
||||
composerView: {
|
||||
composerPlaceholder: ports.composerView.composerPlaceholder,
|
||||
|
|
@ -430,7 +430,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
},
|
||||
);
|
||||
const { pendingRequests, composerSubmit } = conversationControllers;
|
||||
const { messageRenderer } = conversationControllers;
|
||||
const { messageStreamRenderer } = conversationControllers;
|
||||
composerController = conversationControllers.composerController;
|
||||
const { scheduleAppServerWarmup, openView, closeView } = createConnectionLifecycleControllerGroup(
|
||||
{
|
||||
|
|
@ -470,7 +470,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
{
|
||||
connection,
|
||||
composerController,
|
||||
messageRenderer,
|
||||
messageStreamRenderer,
|
||||
serverThreads,
|
||||
serverMetadata,
|
||||
},
|
||||
|
|
@ -517,7 +517,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
},
|
||||
render: {
|
||||
controller: renderController,
|
||||
messages: messageRenderer,
|
||||
messageStream: messageStreamRenderer,
|
||||
openView,
|
||||
closeView,
|
||||
applyViewState,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ import type { HistoryController } from "../threads/history-controller";
|
|||
import type { ChatInboundController } from "../protocol/inbound/controller";
|
||||
import { currentModel, runtimeConfigOrDefault } from "../runtime/effective";
|
||||
import type { RuntimeSnapshot } from "../runtime/snapshot";
|
||||
import { ChatMessageRenderer } from "../ui/message-stream/renderer";
|
||||
import { MessageStreamRenderer } from "../ui/message-stream/renderer";
|
||||
import type { DisplayDetailSection } from "../display/types";
|
||||
import type { ChatMessageScrollIntentController } from "../ui/message-scroll-intent-controller";
|
||||
import type { ChatMessageScrollIntentController } from "../ui/message-stream/scroll-intent-controller";
|
||||
import type { ChatTurnDiffViewState } from "../turn-diff/model";
|
||||
import type { ComposerMetaViewModel } from "../ui/composer";
|
||||
import type { CodexPanelSettings } from "../../../settings/model";
|
||||
|
|
@ -50,7 +50,7 @@ interface ConversationSurfaceControllerGroupPorts {
|
|||
now: () => void;
|
||||
schedule: () => void;
|
||||
};
|
||||
messages: {
|
||||
messageStream: {
|
||||
pendingRequestsSignature: () => string;
|
||||
};
|
||||
composerView: {
|
||||
|
|
@ -98,7 +98,7 @@ export function createConversationSurfaceControllerGroup(
|
|||
history: HistoryController;
|
||||
},
|
||||
) {
|
||||
const { plugin, state, render, messages, composerView, runtime, thread, liveState, status, lifecycle, client, scroll } = context;
|
||||
const { plugin, state, render, messageStream, composerView, runtime, thread, liveState, status, lifecycle, client, scroll } = context;
|
||||
const { app, owner, viewId } = context.obsidian;
|
||||
const stateStore = state.stateStore;
|
||||
const currentClient = client.getClient;
|
||||
|
|
@ -125,7 +125,7 @@ export function createConversationSurfaceControllerGroup(
|
|||
toggleFast: () => void refs.runtimeSettings.toggleFastMode(),
|
||||
onDraftChange: liveState.refresh,
|
||||
onHeightChange: () => {
|
||||
messageRenderer.repinMessagesToBottomIfPinned();
|
||||
messageStreamRenderer.repinMessageStreamToBottomIfPinned();
|
||||
},
|
||||
});
|
||||
const pendingRequests = new PendingRequestController({
|
||||
|
|
@ -228,7 +228,7 @@ export function createConversationSurfaceControllerGroup(
|
|||
},
|
||||
});
|
||||
|
||||
const messageRenderer = new ChatMessageRenderer({
|
||||
const messageStreamRenderer = new MessageStreamRenderer({
|
||||
obsidian: {
|
||||
app,
|
||||
owner,
|
||||
|
|
@ -252,7 +252,7 @@ export function createConversationSurfaceControllerGroup(
|
|||
openTurnDiff: (state) => void plugin.openTurnDiff(state),
|
||||
},
|
||||
requests: {
|
||||
pendingSignature: messages.pendingRequestsSignature,
|
||||
pendingSignature: messageStream.pendingRequestsSignature,
|
||||
pendingSnapshot: () => pendingRequests.snapshot(),
|
||||
pendingActions: () => pendingRequests.actions(),
|
||||
consumePendingAutoFocus: () => pendingRequests.consumeAutoFocus(),
|
||||
|
|
@ -278,13 +278,13 @@ export function createConversationSurfaceControllerGroup(
|
|||
composerController.setActionHandlers({
|
||||
submit: () => void composerSubmit.submit(),
|
||||
threadScrollFromComposer: (action) => {
|
||||
messageRenderer.scrollFromComposer(action);
|
||||
messageStreamRenderer.scrollFromComposer(action);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
pendingRequests,
|
||||
messageRenderer,
|
||||
messageStreamRenderer,
|
||||
composerController,
|
||||
composerSubmit,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { ApprovalAction, PendingApproval } from "../../protocol/server-requ
|
|||
import type { ChatInboundController } from "../../protocol/inbound/controller";
|
||||
import { pendingRequestFocusSignature } from "./signatures";
|
||||
import { pendingRequestBlockSnapshot, type PendingRequestBlockSnapshot } from "./snapshot";
|
||||
import type { PendingRequestBlockActions } from "../../ui/pending-request-block";
|
||||
import { answersForPendingUserInput, type PendingUserInput } from "../../protocol/server-requests/user-input";
|
||||
import type { PendingRequestBlockActions, PendingRequestId } from "./view-model";
|
||||
|
||||
export interface PendingRequestControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
|
|
@ -17,14 +17,14 @@ export interface PendingRequestControllerHost {
|
|||
export class PendingRequestController {
|
||||
private lastFocusSignature = "";
|
||||
private readonly blockActions: PendingRequestBlockActions = {
|
||||
resolveApproval: (approval, action) => {
|
||||
this.resolveApproval(approval, action);
|
||||
resolveApproval: (requestId, action) => {
|
||||
this.resolveApproval(requestId, action);
|
||||
},
|
||||
resolveUserInput: (input) => {
|
||||
this.resolveUserInput(input);
|
||||
resolveUserInput: (requestId) => {
|
||||
this.resolveUserInput(requestId);
|
||||
},
|
||||
cancelUserInput: (input) => {
|
||||
this.cancelUserInput(input);
|
||||
cancelUserInput: (requestId) => {
|
||||
this.cancelUserInput(requestId);
|
||||
},
|
||||
setOpenDetail: (key, open) => {
|
||||
this.host.stateStore.dispatch({ type: "ui/detail-open-set", key, open });
|
||||
|
|
@ -44,12 +44,16 @@ export class PendingRequestController {
|
|||
return this.blockActions;
|
||||
}
|
||||
|
||||
resolveApproval(approval: PendingApproval, action: ApprovalAction): void {
|
||||
resolveApproval(requestId: PendingRequestId, action: ApprovalAction): void {
|
||||
const approval = this.pendingApproval(requestId);
|
||||
if (!approval) return;
|
||||
this.host.controller.resolveApproval(approval, action);
|
||||
this.commitRequestAction();
|
||||
}
|
||||
|
||||
resolveUserInput(input: PendingUserInput): void {
|
||||
resolveUserInput(requestId: PendingRequestId): void {
|
||||
const input = this.pendingUserInput(requestId);
|
||||
if (!input) return;
|
||||
this.host.controller.resolveUserInput(
|
||||
input,
|
||||
answersForPendingUserInput(input, pendingRequestBlockSnapshot(this.host.stateStore.getState()).userInputDrafts),
|
||||
|
|
@ -57,19 +61,29 @@ export class PendingRequestController {
|
|||
this.commitRequestAction();
|
||||
}
|
||||
|
||||
cancelUserInput(input: PendingUserInput): void {
|
||||
cancelUserInput(requestId: PendingRequestId): void {
|
||||
const input = this.pendingUserInput(requestId);
|
||||
if (!input) return;
|
||||
this.host.controller.cancelUserInput(input);
|
||||
this.commitRequestAction();
|
||||
}
|
||||
|
||||
private pendingApproval(requestId: PendingRequestId): PendingApproval | null {
|
||||
return this.host.stateStore.getState().requests.approvals.find((approval) => approval.requestId === requestId) ?? null;
|
||||
}
|
||||
|
||||
private pendingUserInput(requestId: PendingRequestId): PendingUserInput | null {
|
||||
return this.host.stateStore.getState().requests.pendingUserInputs.find((input) => input.requestId === requestId) ?? null;
|
||||
}
|
||||
|
||||
private commitRequestAction(): void {
|
||||
this.host.refreshLiveState();
|
||||
this.host.render();
|
||||
}
|
||||
|
||||
consumeAutoFocus(): boolean {
|
||||
const state = pendingRequestBlockSnapshot(this.host.stateStore.getState());
|
||||
const signature = pendingRequestFocusSignature(state.approvals, state.pendingUserInputs);
|
||||
const state = this.host.stateStore.getState();
|
||||
const signature = pendingRequestFocusSignature(state.requests.approvals, state.requests.pendingUserInputs);
|
||||
if (!signature) {
|
||||
this.lastFocusSignature = "";
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,39 @@
|
|||
import type { ChatState } from "../../state/reducer";
|
||||
import type { PendingApproval } from "../../protocol/server-requests/approval";
|
||||
import type { PendingUserInput } from "../../protocol/server-requests/user-input";
|
||||
import {
|
||||
pendingApprovalViewModel,
|
||||
pendingUserInputViewModel,
|
||||
type PendingApprovalViewModel,
|
||||
type PendingUserInputViewModel,
|
||||
} from "./view-model";
|
||||
|
||||
export interface PendingRequestBlockSnapshot {
|
||||
approvals: readonly PendingApproval[];
|
||||
pendingUserInputs: readonly PendingUserInput[];
|
||||
approvals: readonly PendingApprovalViewModel[];
|
||||
pendingUserInputs: readonly PendingUserInputViewModel[];
|
||||
userInputDrafts: ReadonlyMap<string, string>;
|
||||
openDetails: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export function pendingRequestBlockSnapshot(state: ChatState): PendingRequestBlockSnapshot {
|
||||
return {
|
||||
return pendingRequestBlockSnapshotFromRequests({
|
||||
approvals: state.requests.approvals,
|
||||
pendingUserInputs: state.requests.pendingUserInputs,
|
||||
userInputDrafts: state.requests.userInputDrafts,
|
||||
openDetails: state.ui.openDetails,
|
||||
});
|
||||
}
|
||||
|
||||
export function pendingRequestBlockSnapshotFromRequests(source: {
|
||||
approvals: readonly PendingApproval[];
|
||||
pendingUserInputs: readonly PendingUserInput[];
|
||||
userInputDrafts: ReadonlyMap<string, string>;
|
||||
openDetails: ReadonlySet<string>;
|
||||
}): PendingRequestBlockSnapshot {
|
||||
return {
|
||||
approvals: source.approvals.map(pendingApprovalViewModel),
|
||||
pendingUserInputs: source.pendingUserInputs.map(pendingUserInputViewModel),
|
||||
userInputDrafts: source.userInputDrafts,
|
||||
openDetails: source.openDetails,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import type { RequestId } from "../../../../app-server/connection/rpc-messages";
|
||||
import {
|
||||
approvalActionOptions,
|
||||
approvalDetails,
|
||||
approvalSummary,
|
||||
approvalTitle,
|
||||
type ApprovalAction,
|
||||
type PendingApproval,
|
||||
} from "../../protocol/server-requests/approval";
|
||||
import {
|
||||
questionDefaultAnswer,
|
||||
userInputDraftKey,
|
||||
userInputOtherDraftKey,
|
||||
type PendingUserInput,
|
||||
} from "../../protocol/server-requests/user-input";
|
||||
|
||||
export type PendingRequestId = RequestId;
|
||||
export type PendingRequestApprovalAction = ApprovalAction;
|
||||
|
||||
export interface PendingRequestApprovalOption {
|
||||
label: string;
|
||||
className: string;
|
||||
action: PendingRequestApprovalAction;
|
||||
}
|
||||
|
||||
export interface PendingRequestDetailRow {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PendingApprovalViewModel {
|
||||
requestId: PendingRequestId;
|
||||
title: string;
|
||||
summary: string;
|
||||
details: PendingRequestDetailRow[];
|
||||
actions: PendingRequestApprovalOption[];
|
||||
}
|
||||
|
||||
export interface PendingUserInputOptionViewModel {
|
||||
label: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface PendingUserInputQuestionViewModel {
|
||||
id: string;
|
||||
header?: string | null;
|
||||
question: string;
|
||||
isOther: boolean;
|
||||
isSecret: boolean;
|
||||
defaultAnswer: string;
|
||||
draftKey: string;
|
||||
otherDraftKey: string;
|
||||
options: PendingUserInputOptionViewModel[] | null;
|
||||
}
|
||||
|
||||
export interface PendingUserInputViewModel {
|
||||
requestId: PendingRequestId;
|
||||
title: string;
|
||||
body: string;
|
||||
questions: PendingUserInputQuestionViewModel[];
|
||||
}
|
||||
|
||||
export interface PendingRequestBlockActions {
|
||||
resolveApproval: (requestId: PendingRequestId, action: PendingRequestApprovalAction) => void;
|
||||
resolveUserInput: (requestId: PendingRequestId) => void;
|
||||
cancelUserInput: (requestId: PendingRequestId) => void;
|
||||
setOpenDetail?: (key: string, open: boolean) => void;
|
||||
setUserInputDraft: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
export function pendingApprovalViewModel(approval: PendingApproval): PendingApprovalViewModel {
|
||||
return {
|
||||
requestId: approval.requestId,
|
||||
title: approvalTitle(approval),
|
||||
summary: approvalSummary(approval),
|
||||
details: approvalDetails(approval),
|
||||
actions: approvalActionOptions(approval),
|
||||
};
|
||||
}
|
||||
|
||||
export function pendingUserInputViewModel(input: PendingUserInput): PendingUserInputViewModel {
|
||||
return {
|
||||
requestId: input.requestId,
|
||||
title: "Codex needs input",
|
||||
body: `Answer ${String(input.params.questions.length)} Plan mode question${input.params.questions.length === 1 ? "" : "s"} to continue.`,
|
||||
questions: input.params.questions.map((question) => ({
|
||||
id: question.id,
|
||||
header: question.header,
|
||||
question: question.question,
|
||||
isOther: question.isOther,
|
||||
isSecret: question.isSecret,
|
||||
defaultAnswer: questionDefaultAnswer(question),
|
||||
draftKey: userInputDraftKey(input.requestId, question.id),
|
||||
otherDraftKey: userInputOtherDraftKey(input.requestId, question.id),
|
||||
options: question.options,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ import { closeChatView, openChatView, type ChatViewLifecycleHost } from "./view-
|
|||
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "./regions/toolbar";
|
||||
import { ChatViewRenderController } from "./view-render-controller";
|
||||
import { applyChatViewState } from "./view-state";
|
||||
import type { ChatMessageRenderer } from "../ui/message-stream/renderer";
|
||||
import type { MessageStreamRenderer } from "../ui/message-stream/renderer";
|
||||
import { applyCachedSharedAppServerState, type CachedSharedAppServerStateSource } from "./cached-app-server-state";
|
||||
import type { ChatViewDeferredTasks, RestoredThreadState } from "../lifecycle";
|
||||
import { createChatShellRenderPort } from "./shell-render";
|
||||
|
|
@ -30,7 +30,7 @@ interface ViewRenderControllerGroupPorts {
|
|||
panelRoot: () => HTMLElement | null;
|
||||
toolbarNode: () => UiNode;
|
||||
goalNode: () => UiNode;
|
||||
messagesNode: () => UiNode;
|
||||
messageStreamNode: () => UiNode;
|
||||
composerNode: () => UiNode;
|
||||
};
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ export function createViewRenderControllerGroup(context: ViewRenderControllerGro
|
|||
showToolbar: () => plugin.settings.showToolbar,
|
||||
toolbarNode: context.render.toolbarNode,
|
||||
goalNode: context.render.goalNode,
|
||||
messagesNode: context.render.messagesNode,
|
||||
messageStreamNode: context.render.messageStreamNode,
|
||||
composerNode: context.render.composerNode,
|
||||
}),
|
||||
panelRoot: render.panelRoot,
|
||||
|
|
@ -90,7 +90,7 @@ export function createConnectionLifecycleControllerGroup(
|
|||
refs: {
|
||||
connection: ConnectionManager;
|
||||
composerController: ChatComposerController;
|
||||
messageRenderer: ChatMessageRenderer;
|
||||
messageStreamRenderer: MessageStreamRenderer;
|
||||
serverThreads: ChatServerThreadActions;
|
||||
serverMetadata: ChatServerMetadataActions;
|
||||
},
|
||||
|
|
@ -128,7 +128,7 @@ export function createConnectionLifecycleControllerGroup(
|
|||
},
|
||||
panelRoot: render.panelRoot,
|
||||
disposeMessages: () => {
|
||||
refs.messageRenderer.dispose();
|
||||
refs.messageStreamRenderer.dispose();
|
||||
},
|
||||
disposeComposer: () => {
|
||||
refs.composerController.dispose();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GoalBannerActions, GoalBannerOptions } from "../../ui/goal-banner";
|
||||
import type { GoalRegionActions, GoalRegionOptions } from "../../ui/goal";
|
||||
import type { ChatPanelGoalPorts } from "./ports";
|
||||
|
||||
export function chatPanelGoalProps(
|
||||
|
|
@ -6,8 +6,8 @@ export function chatPanelGoalProps(
|
|||
state: ReturnType<ChatPanelGoalPorts["state"]["chat"]>,
|
||||
): {
|
||||
goal: ReturnType<ChatPanelGoalPorts["state"]["chat"]>["activeThread"]["goal"];
|
||||
actions: GoalBannerActions;
|
||||
options: GoalBannerOptions;
|
||||
actions: GoalRegionActions;
|
||||
options: GoalRegionOptions;
|
||||
} {
|
||||
const goal = state.activeThread.goal;
|
||||
const goalThreadId = goal?.threadId ?? null;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { pendingRequestsSignature as requestStateSignature } from "../../conversation/pending-requests/signatures";
|
||||
import type { ChatPanelMessagesPorts } from "./ports";
|
||||
import type { ChatPanelMessageStreamPorts } from "./ports";
|
||||
|
||||
export function chatPanelMessagesNode(ports: ChatPanelMessagesPorts) {
|
||||
export function chatPanelMessageStreamNode(ports: ChatPanelMessageStreamPorts) {
|
||||
return ports.render.node();
|
||||
}
|
||||
|
||||
export function chatPanelPendingRequestsSignature(ports: ChatPanelMessagesPorts): string {
|
||||
export function chatPanelMessageStreamPendingRequestsSignature(ports: ChatPanelMessageStreamPorts): string {
|
||||
const state = ports.state.chat();
|
||||
return requestStateSignature(state.requests.approvals, state.requests.pendingUserInputs, state.requests.userInputDrafts);
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ export interface ChatPanelGoalPorts extends ChatPanelStatePort {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ChatPanelMessagesPorts extends ChatPanelStatePort {
|
||||
export interface ChatPanelMessageStreamPorts extends ChatPanelStatePort {
|
||||
render: {
|
||||
node: () => UiNode;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import type { ComponentChild as UiNode } from "preact";
|
|||
import { useComputed } from "@preact/signals";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { goalBannerNode } from "../../ui/goal-banner";
|
||||
import { goalRegionNode } from "../../ui/goal";
|
||||
import { useChatPanelShellState } from "../../ui/shell";
|
||||
import { toolbarNode } from "../../ui/toolbar";
|
||||
import { chatPanelGoalProps } from "./goal";
|
||||
import { chatPanelMessagesNode } from "./messages";
|
||||
import type { ChatPanelGoalPorts, ChatPanelMessagesPorts, ChatPanelToolbarPorts } from "./ports";
|
||||
import { chatPanelMessageStreamNode } from "./message-stream";
|
||||
import type { ChatPanelGoalPorts, ChatPanelMessageStreamPorts, ChatPanelToolbarPorts } from "./ports";
|
||||
import { chatPanelToolbarViewModel } from "./toolbar";
|
||||
|
||||
export function chatPanelToolbarRegionNode(ports: ChatPanelToolbarPorts): UiNode {
|
||||
|
|
@ -18,8 +18,8 @@ export function chatPanelGoalRegionNode(ports: ChatPanelGoalPorts): UiNode {
|
|||
return <GoalRegion ports={ports} />;
|
||||
}
|
||||
|
||||
export function chatPanelMessagesRegionNode(ports: ChatPanelMessagesPorts): UiNode {
|
||||
return <MessagesRegion ports={ports} />;
|
||||
export function chatPanelMessageStreamRegionNode(ports: ChatPanelMessageStreamPorts): UiNode {
|
||||
return <MessageStreamRegion ports={ports} />;
|
||||
}
|
||||
|
||||
export function chatPanelComposerRegionNode(node: () => UiNode): UiNode {
|
||||
|
|
@ -44,10 +44,10 @@ function GoalRegion({ ports }: { ports: ChatPanelGoalPorts }): UiNode {
|
|||
ui: ui.value,
|
||||
});
|
||||
});
|
||||
return goalBannerNode(props.value.goal, props.value.actions, props.value.options);
|
||||
return goalRegionNode(props.value.goal, props.value.actions, props.value.options);
|
||||
}
|
||||
|
||||
function MessagesRegion({ ports }: { ports: ChatPanelMessagesPorts }): UiNode {
|
||||
function MessageStreamRegion({ ports }: { ports: ChatPanelMessageStreamPorts }): UiNode {
|
||||
const { activeThread, runtime, messageStream, requests, turn, ui, renderVersion } = useChatPanelShellState();
|
||||
void activeThread.value;
|
||||
void runtime.value;
|
||||
|
|
@ -56,7 +56,7 @@ function MessagesRegion({ ports }: { ports: ChatPanelMessagesPorts }): UiNode {
|
|||
void turn.value;
|
||||
void ui.value;
|
||||
void renderVersion.value;
|
||||
return chatPanelMessagesNode(ports);
|
||||
return chatPanelMessageStreamNode(ports);
|
||||
}
|
||||
|
||||
function ComposerRegion({ node }: { node: () => UiNode }): UiNode {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function createChatShellRenderPort(
|
|||
showToolbar: () => boolean;
|
||||
toolbarNode: () => UiNode;
|
||||
goalNode: () => UiNode;
|
||||
messagesNode: () => UiNode;
|
||||
messageStreamNode: () => UiNode;
|
||||
composerNode: () => UiNode;
|
||||
},
|
||||
): ChatShellRenderPort {
|
||||
|
|
@ -24,7 +24,7 @@ export function createChatShellRenderPort(
|
|||
showToolbar: options.showToolbar(),
|
||||
toolbarNode: options.toolbarNode,
|
||||
goalNode: options.goalNode,
|
||||
messagesNode: options.messagesNode,
|
||||
messageStreamNode: options.messageStreamNode,
|
||||
composerNode: options.composerNode,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,31 +6,31 @@ import { isComposerSendKey, type SendShortcut } from "../../../shared/ui/keyboar
|
|||
import { IconButton } from "../../../shared/ui/components";
|
||||
import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow";
|
||||
|
||||
export interface GoalBannerActions {
|
||||
export interface GoalRegionActions {
|
||||
onSave: (objective: string, tokenBudget: number | null) => void;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export interface GoalBannerOptions {
|
||||
export interface GoalRegionOptions {
|
||||
sendShortcut: SendShortcut;
|
||||
editingRequested?: boolean | undefined;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
}
|
||||
|
||||
export function goalBannerNode(goal: ThreadGoal | null, actions: GoalBannerActions, options: GoalBannerOptions): UiNode {
|
||||
return <GoalBanner goal={goal} actions={actions} options={options} />;
|
||||
export function goalRegionNode(goal: ThreadGoal | null, actions: GoalRegionActions, options: GoalRegionOptions): UiNode {
|
||||
return <GoalRegion goal={goal} actions={actions} options={options} />;
|
||||
}
|
||||
|
||||
function GoalBanner({
|
||||
function GoalRegion({
|
||||
goal,
|
||||
actions,
|
||||
options,
|
||||
}: {
|
||||
goal: ThreadGoal | null;
|
||||
actions: GoalBannerActions;
|
||||
options: GoalBannerOptions;
|
||||
actions: GoalRegionActions;
|
||||
options: GoalRegionOptions;
|
||||
}): UiNode {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [objective, setObjective] = useState(goal?.objective ?? "");
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import type { ChatState } from "../../state/reducer";
|
||||
import { chatTurnBusy } from "../../state/reducer";
|
||||
import type { DisplayItem } from "../../display/types";
|
||||
import { implementPlanCandidateFromState } from "../../state/selectors";
|
||||
import {
|
||||
forkCandidatesFromItems,
|
||||
isForkCandidateItem,
|
||||
isRollbackCandidateItem,
|
||||
rollbackCandidateFromItems,
|
||||
} from "../../display/item-actions";
|
||||
import type { MessageStreamContext } from "./context";
|
||||
import type { ChatMessageStreamContextPort } from "./ports";
|
||||
|
||||
export function createMessageStreamContext(state: ChatState, port: ChatMessageStreamContextPort): MessageStreamContext {
|
||||
const busy = chatTurnBusy(state);
|
||||
const rollbackCandidate = busy ? null : rollbackCandidateFromItems(state.messageStream.displayItems);
|
||||
const forkCandidates = busy ? [] : forkCandidatesFromItems(state.messageStream.displayItems);
|
||||
const implementPlanCandidate = implementPlanCandidateFromState(state);
|
||||
|
||||
return {
|
||||
activeThreadId: state.activeThread.id,
|
||||
turnLifecycle: state.turn.lifecycle,
|
||||
historyCursor: state.messageStream.historyCursor,
|
||||
loadingHistory: state.messageStream.loadingHistory,
|
||||
displayItems: state.messageStream.displayItems,
|
||||
turnDiffs: state.messageStream.turnDiffs,
|
||||
workspaceRoot: state.activeThread.cwd ?? port.vaultPath,
|
||||
openDetails: state.ui.openDetails,
|
||||
onDetailsToggle: port.setOpenDetail,
|
||||
loadOlderTurns: port.loadOlderTurns,
|
||||
renderMarkdown: port.renderMarkdown,
|
||||
copyText: port.copyMessageText,
|
||||
canImplementPlanItem: (item: DisplayItem) => item.id === implementPlanCandidate?.id,
|
||||
onImplementPlanItem: (item) => {
|
||||
port.actions.implementPlan(item);
|
||||
},
|
||||
canRollbackItem: (item: DisplayItem) => isRollbackCandidateItem(item, rollbackCandidate),
|
||||
onRollbackItem: () => {
|
||||
if (state.activeThread.id) port.actions.rollbackThread(state.activeThread.id);
|
||||
},
|
||||
canForkItem: (item: DisplayItem) => isForkCandidateItem(item, forkCandidates),
|
||||
onForkItem: (item, archiveSource) => {
|
||||
if (state.activeThread.id && item.turnId) {
|
||||
port.actions.forkThreadFromTurn(state.activeThread.id, item.turnId, archiveSource);
|
||||
}
|
||||
},
|
||||
openTurnDiff: (turnDiffState) => {
|
||||
port.actions.openTurnDiff(turnDiffState);
|
||||
},
|
||||
pendingRequests: {
|
||||
signature: port.requests.pendingSignature(),
|
||||
snapshot: port.requests.pendingSnapshot,
|
||||
actions: port.requests.pendingActions,
|
||||
consumeAutoFocus: port.requests.consumePendingAutoFocus,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ import type { ComponentChild as UiNode } from "preact";
|
|||
|
||||
import type { ChatTurnLifecycleState } from "../../state/reducer";
|
||||
import type { PendingRequestBlockSnapshot } from "../../conversation/pending-requests/snapshot";
|
||||
import type { PendingRequestBlockActions } from "../../conversation/pending-requests/view-model";
|
||||
import type { DisplayItem } from "../../display/types";
|
||||
import type { PendingRequestBlockActions } from "../pending-request-block";
|
||||
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
|
||||
|
||||
export interface MessageStreamBlock {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { MarkdownRenderer, type App, type Component } from "obsidian";
|
||||
import { MarkdownRenderer, Notice, type App, type Component } from "obsidian";
|
||||
|
||||
import { notifyMessageContentRendered } from "../message-content-events";
|
||||
import { bindRenderedMarkdownFileLinks, bindRenderedWikiLinks } from "./rendered-markdown-links";
|
||||
import { isAbsoluteFileHref, vaultFileLinkTarget, vaultRelativeFileLinkTarget } from "../../../../shared/obsidian/file-links";
|
||||
import { notifyMessageContentRendered } from "./content-events";
|
||||
|
||||
export interface MarkdownMessageRendererOptions {
|
||||
app: App;
|
||||
|
|
@ -28,3 +28,40 @@ export class MarkdownMessageRenderer {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface RenderedMarkdownLinkContext {
|
||||
app: App;
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function bindRenderedWikiLinks(parent: HTMLElement, sourcePath: string, context: RenderedMarkdownLinkContext): void {
|
||||
parent.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((link) => {
|
||||
link.addClass("codex-panel__wikilink");
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
const href = link.getAttribute("data-href") ?? link.getAttribute("href") ?? link.textContent;
|
||||
const target = vaultRelativeFileLinkTarget(context.vaultPath, context.app.vault.configDir, href) ?? href;
|
||||
if (target === href && isAbsoluteFileHref(href)) {
|
||||
new Notice("Cannot open files outside the vault.");
|
||||
return;
|
||||
}
|
||||
if (target.trim().length > 0) {
|
||||
void context.app.workspace.openLinkText(target, sourcePath, false);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function bindRenderedMarkdownFileLinks(parent: HTMLElement, sourcePath: string, context: RenderedMarkdownLinkContext): void {
|
||||
parent.querySelectorAll<HTMLAnchorElement>("a[href]:not(.internal-link)").forEach((link) => {
|
||||
const href = link.getAttribute("href") ?? "";
|
||||
const target = vaultFileLinkTarget(context.app, context.vaultPath, href);
|
||||
if (!target) return;
|
||||
|
||||
link.addClass("codex-panel__filelink");
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
void context.app.workspace.openLinkText(target, sourcePath, false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,37 +2,17 @@ import type { ComponentChild as UiNode } from "preact";
|
|||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
approvalActionOptions,
|
||||
approvalDetails,
|
||||
approvalSummary,
|
||||
approvalTitle,
|
||||
type ApprovalAction,
|
||||
type PendingApproval,
|
||||
} from "../protocol/server-requests/approval";
|
||||
import type { PendingUserInput } from "../protocol/server-requests/user-input";
|
||||
import { questionDefaultAnswer } from "../protocol/server-requests/user-input";
|
||||
type PendingApprovalViewModel,
|
||||
type PendingRequestBlockActions,
|
||||
type PendingUserInputQuestionViewModel,
|
||||
type PendingUserInputViewModel,
|
||||
} from "../../conversation/pending-requests/view-model";
|
||||
import { createWorkMessageClassName } from "./work-message";
|
||||
|
||||
type PendingUserInputRequestId = PendingUserInput["requestId"];
|
||||
|
||||
export interface PendingRequestBlockActions {
|
||||
resolveApproval: (approval: PendingApproval, action: ApprovalAction) => void;
|
||||
resolveUserInput: (input: PendingUserInput) => void;
|
||||
cancelUserInput: (input: PendingUserInput) => void;
|
||||
setOpenDetail?: (key: string, open: boolean) => void;
|
||||
setUserInputDraft: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
export interface PendingRequestBlockDrafts {
|
||||
values: ReadonlyMap<string, string>;
|
||||
draftKey: (requestId: PendingUserInputRequestId, questionId: string) => string;
|
||||
otherDraftKey: (requestId: PendingUserInputRequestId, questionId: string) => string;
|
||||
}
|
||||
|
||||
export function pendingRequestBlockNode(
|
||||
approvals: readonly PendingApproval[],
|
||||
pendingUserInputs: readonly PendingUserInput[],
|
||||
drafts: PendingRequestBlockDrafts,
|
||||
approvals: readonly PendingApprovalViewModel[],
|
||||
pendingUserInputs: readonly PendingUserInputViewModel[],
|
||||
userInputDrafts: ReadonlyMap<string, string>,
|
||||
openDetails: ReadonlySet<string>,
|
||||
actions: PendingRequestBlockActions,
|
||||
autoFocusRequested = false,
|
||||
|
|
@ -43,7 +23,7 @@ export function pendingRequestBlockNode(
|
|||
<PendingRequestBlock
|
||||
approvals={approvals}
|
||||
pendingUserInputs={pendingUserInputs}
|
||||
drafts={drafts}
|
||||
userInputDrafts={userInputDrafts}
|
||||
openDetails={openDetails}
|
||||
actions={actions}
|
||||
autoFocusRequested={autoFocusRequested}
|
||||
|
|
@ -56,16 +36,16 @@ export function pendingRequestBlockNode(
|
|||
function PendingRequestBlock({
|
||||
approvals,
|
||||
pendingUserInputs,
|
||||
drafts,
|
||||
userInputDrafts,
|
||||
openDetails,
|
||||
actions,
|
||||
autoFocusRequested,
|
||||
consumeAutoFocus,
|
||||
autoFocusSignature,
|
||||
}: {
|
||||
approvals: readonly PendingApproval[];
|
||||
pendingUserInputs: readonly PendingUserInput[];
|
||||
drafts: PendingRequestBlockDrafts;
|
||||
approvals: readonly PendingApprovalViewModel[];
|
||||
pendingUserInputs: readonly PendingUserInputViewModel[];
|
||||
userInputDrafts: ReadonlyMap<string, string>;
|
||||
openDetails: ReadonlySet<string>;
|
||||
actions: PendingRequestBlockActions;
|
||||
autoFocusRequested: boolean;
|
||||
|
|
@ -87,7 +67,7 @@ function PendingRequestBlock({
|
|||
<ApprovalCard key={String(approval.requestId)} approval={approval} openDetails={openDetails} actions={actions} />
|
||||
))}
|
||||
{pendingUserInputs.map((input) => (
|
||||
<UserInputCard key={String(input.requestId)} input={input} drafts={drafts} actions={actions} />
|
||||
<UserInputCard key={String(input.requestId)} input={input} userInputDrafts={userInputDrafts} actions={actions} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -114,25 +94,25 @@ function ApprovalCard({
|
|||
openDetails,
|
||||
actions,
|
||||
}: {
|
||||
approval: PendingApproval;
|
||||
approval: PendingApprovalViewModel;
|
||||
openDetails: ReadonlySet<string>;
|
||||
actions: PendingRequestBlockActions;
|
||||
}): UiNode {
|
||||
return (
|
||||
<PendingRequestCard className="codex-panel__approval">
|
||||
<div className="codex-panel__pending-request-info">
|
||||
<div className="codex-panel__pending-request-title">{approvalTitle(approval)}</div>
|
||||
<div className="codex-panel__pending-request-body">{approvalSummary(approval)}</div>
|
||||
<div className="codex-panel__pending-request-title">{approval.title}</div>
|
||||
<div className="codex-panel__pending-request-body">{approval.summary}</div>
|
||||
<ApprovalDetails approval={approval} openDetails={openDetails} actions={actions} />
|
||||
</div>
|
||||
<div className="codex-panel__pending-request-actions">
|
||||
{approvalActionOptions(approval).map((option) => (
|
||||
{approval.actions.map((option) => (
|
||||
<ActionButton
|
||||
key={option.label}
|
||||
label={option.label}
|
||||
className={option.className}
|
||||
onClick={() => {
|
||||
actions.resolveApproval(approval, option.action);
|
||||
actions.resolveApproval(approval.requestId, option.action);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -146,7 +126,7 @@ function ApprovalDetails({
|
|||
openDetails,
|
||||
actions,
|
||||
}: {
|
||||
approval: PendingApproval;
|
||||
approval: PendingApprovalViewModel;
|
||||
openDetails: ReadonlySet<string>;
|
||||
actions: PendingRequestBlockActions;
|
||||
}): UiNode {
|
||||
|
|
@ -161,7 +141,7 @@ function ApprovalDetails({
|
|||
>
|
||||
<summary tabIndex={-1}>Request details</summary>
|
||||
<dl className="codex-panel__meta-grid">
|
||||
{approvalDetails(approval).map((row) => (
|
||||
{approval.details.map((row) => (
|
||||
<MetaPair key={`${row.key}:${row.value}`} name={row.key} value={row.value} />
|
||||
))}
|
||||
</dl>
|
||||
|
|
@ -171,35 +151,33 @@ function ApprovalDetails({
|
|||
|
||||
function UserInputCard({
|
||||
input,
|
||||
drafts,
|
||||
userInputDrafts,
|
||||
actions,
|
||||
}: {
|
||||
input: PendingUserInput;
|
||||
drafts: PendingRequestBlockDrafts;
|
||||
input: PendingUserInputViewModel;
|
||||
userInputDrafts: ReadonlyMap<string, string>;
|
||||
actions: PendingRequestBlockActions;
|
||||
}): UiNode {
|
||||
return (
|
||||
<PendingRequestCard className="codex-panel__user-input">
|
||||
<div className="codex-panel__pending-request-info">
|
||||
<div className="codex-panel__pending-request-title">Codex needs input</div>
|
||||
<div className="codex-panel__pending-request-body">
|
||||
Answer {String(input.params.questions.length)} Plan mode question{input.params.questions.length === 1 ? "" : "s"} to continue.
|
||||
</div>
|
||||
<UserInputQuestions input={input} drafts={drafts} actions={actions} />
|
||||
<div className="codex-panel__pending-request-title">{input.title}</div>
|
||||
<div className="codex-panel__pending-request-body">{input.body}</div>
|
||||
<UserInputQuestions input={input} userInputDrafts={userInputDrafts} actions={actions} />
|
||||
</div>
|
||||
<div className="codex-panel__pending-request-actions">
|
||||
<ActionButton
|
||||
label="Submit"
|
||||
className="mod-cta"
|
||||
onClick={() => {
|
||||
actions.resolveUserInput(input);
|
||||
actions.resolveUserInput(input.requestId);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Cancel"
|
||||
className=""
|
||||
onClick={() => {
|
||||
actions.cancelUserInput(input);
|
||||
actions.cancelUserInput(input.requestId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -213,18 +191,17 @@ function PendingRequestCard({ className, children }: { className: string; childr
|
|||
|
||||
function UserInputQuestions({
|
||||
input,
|
||||
drafts,
|
||||
userInputDrafts,
|
||||
actions,
|
||||
}: {
|
||||
input: PendingUserInput;
|
||||
drafts: PendingRequestBlockDrafts;
|
||||
input: PendingUserInputViewModel;
|
||||
userInputDrafts: ReadonlyMap<string, string>;
|
||||
actions: PendingRequestBlockActions;
|
||||
}): UiNode {
|
||||
return (
|
||||
<>
|
||||
{input.params.questions.map((question) => {
|
||||
const draftKey = drafts.draftKey(input.requestId, question.id);
|
||||
const current = drafts.values.get(draftKey) ?? questionDefaultAnswer(question);
|
||||
{input.questions.map((question) => {
|
||||
const current = userInputDrafts.get(question.draftKey) ?? question.defaultAnswer;
|
||||
return (
|
||||
<div key={question.id} className="codex-panel__user-input-question">
|
||||
{question.header ? <div className="codex-panel__user-input-header">{question.header}</div> : null}
|
||||
|
|
@ -243,7 +220,7 @@ function UserInputQuestions({
|
|||
value={option.label}
|
||||
checked={current === option.label}
|
||||
onChange={(event) => {
|
||||
if (event.currentTarget.checked) actions.setUserInputDraft(draftKey, option.label);
|
||||
if (event.currentTarget.checked) actions.setUserInputDraft(question.draftKey, option.label);
|
||||
}}
|
||||
/>
|
||||
<span className="codex-panel__user-input-option-label">{option.label}</span>
|
||||
|
|
@ -255,25 +232,22 @@ function UserInputQuestions({
|
|||
})}
|
||||
{question.isOther ? (
|
||||
<OtherUserInputOption
|
||||
input={input}
|
||||
questionId={question.id}
|
||||
questionText={question.question}
|
||||
groupName={`codex-panel-${String(input.requestId)}-${question.id}`}
|
||||
current={current}
|
||||
optionLabels={new Set(question.options.map((option) => option.label))}
|
||||
drafts={drafts}
|
||||
question={question}
|
||||
userInputDrafts={userInputDrafts}
|
||||
actions={actions}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<FreeformUserInput
|
||||
input={input}
|
||||
questionId={question.id}
|
||||
questionText={question.question}
|
||||
isSecret={question.isSecret}
|
||||
current={current}
|
||||
drafts={drafts}
|
||||
question={question}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -286,30 +260,28 @@ function UserInputQuestions({
|
|||
}
|
||||
|
||||
function OtherUserInputOption({
|
||||
input,
|
||||
questionId,
|
||||
questionText,
|
||||
groupName,
|
||||
current,
|
||||
optionLabels,
|
||||
drafts,
|
||||
question,
|
||||
userInputDrafts,
|
||||
actions,
|
||||
}: {
|
||||
input: PendingUserInput;
|
||||
questionId: string;
|
||||
questionText: string;
|
||||
groupName: string;
|
||||
current: string;
|
||||
optionLabels: ReadonlySet<string>;
|
||||
drafts: PendingRequestBlockDrafts;
|
||||
question: PendingUserInputQuestionViewModel;
|
||||
userInputDrafts: ReadonlyMap<string, string>;
|
||||
actions: PendingRequestBlockActions;
|
||||
}): UiNode {
|
||||
const draftKey = drafts.draftKey(input.requestId, questionId);
|
||||
const otherKey = drafts.otherDraftKey(input.requestId, questionId);
|
||||
const otherValue = drafts.values.get(otherKey) ?? "";
|
||||
const draftKey = question.draftKey;
|
||||
const otherKey = question.otherDraftKey;
|
||||
const otherValue = userInputDrafts.get(otherKey) ?? "";
|
||||
const [inputValue, setInputValue] = useState(otherValue);
|
||||
const composingRef = useRef(false);
|
||||
const otherSelected = drafts.values.has(draftKey) && current === otherValue && !optionLabels.has(current);
|
||||
const otherSelected = userInputDrafts.has(draftKey) && current === otherValue && !optionLabels.has(current);
|
||||
useEffect(() => {
|
||||
if (!composingRef.current) setInputValue(otherValue);
|
||||
}, [otherValue]);
|
||||
|
|
@ -369,23 +341,18 @@ function OtherUserInputOption({
|
|||
}
|
||||
|
||||
function FreeformUserInput({
|
||||
input,
|
||||
questionId,
|
||||
questionText,
|
||||
isSecret,
|
||||
current,
|
||||
drafts,
|
||||
question,
|
||||
actions,
|
||||
}: {
|
||||
input: PendingUserInput;
|
||||
questionId: string;
|
||||
questionText: string;
|
||||
isSecret: boolean;
|
||||
current: string;
|
||||
drafts: PendingRequestBlockDrafts;
|
||||
question: PendingUserInputQuestionViewModel;
|
||||
actions: PendingRequestBlockActions;
|
||||
}): UiNode {
|
||||
const draftKey = drafts.draftKey(input.requestId, questionId);
|
||||
return (
|
||||
<input
|
||||
className="codex-panel__user-input-text"
|
||||
|
|
@ -393,7 +360,7 @@ function FreeformUserInput({
|
|||
aria-label={questionText}
|
||||
value={current}
|
||||
onInput={(event) => {
|
||||
actions.setUserInputDraft(draftKey, event.currentTarget.value);
|
||||
actions.setUserInputDraft(question.draftKey, event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ChatAction, ChatState } from "../../state/reducer";
|
||||
import type { PendingRequestBlockSnapshot } from "../../conversation/pending-requests/snapshot";
|
||||
import type { PendingRequestBlockActions } from "../../conversation/pending-requests/view-model";
|
||||
import type { DisplayItem } from "../../display/types";
|
||||
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
|
||||
import type { PendingRequestBlockActions } from "../pending-request-block";
|
||||
|
||||
export interface ChatMessageStreamActionPort {
|
||||
rollbackThread: (threadId: string) => void;
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import type { ChatState } from "../../state/reducer";
|
||||
import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../message-virtualizer";
|
||||
import { messageStreamBlocks } from "./stream-blocks";
|
||||
import { createMessageStreamContext } from "./context-model";
|
||||
import type { ChatMessageStreamContextPort } from "./ports";
|
||||
import type { MessageStreamRenderState } from "./render";
|
||||
|
||||
export interface MessageStreamRenderStateOptions {
|
||||
state: ChatState;
|
||||
contextPort: ChatMessageStreamContextPort;
|
||||
consumeScrollIntent: () => MessageStreamScrollIntent;
|
||||
registerVirtualizer: (virtualizer: MessageStreamVirtualizerHandle) => () => void;
|
||||
}
|
||||
|
||||
export function createMessageStreamRenderState(options: MessageStreamRenderStateOptions): MessageStreamRenderState {
|
||||
return {
|
||||
blocks: messageStreamBlocks(createMessageStreamContext(options.state, options.contextPort)),
|
||||
consumeScrollIntent: options.consumeScrollIntent,
|
||||
registerVirtualizer: options.registerVirtualizer,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { Notice, type App } from "obsidian";
|
||||
|
||||
import { isAbsoluteFileHref, vaultFileLinkTarget, vaultRelativeFileLinkTarget } from "../../../../shared/obsidian/file-links";
|
||||
|
||||
export interface RenderedMarkdownLinkContext {
|
||||
app: App;
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function bindRenderedWikiLinks(parent: HTMLElement, sourcePath: string, context: RenderedMarkdownLinkContext): void {
|
||||
parent.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((link) => {
|
||||
link.addClass("codex-panel__wikilink");
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
const href = link.getAttribute("data-href") ?? link.getAttribute("href") ?? link.textContent;
|
||||
const target = vaultRelativeFileLinkTarget(context.vaultPath, context.app.vault.configDir, href) ?? href;
|
||||
if (target === href && isAbsoluteFileHref(href)) {
|
||||
new Notice("Cannot open files outside the vault.");
|
||||
return;
|
||||
}
|
||||
if (target.trim().length > 0) {
|
||||
void context.app.workspace.openLinkText(target, sourcePath, false);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function bindRenderedMarkdownFileLinks(parent: HTMLElement, sourcePath: string, context: RenderedMarkdownLinkContext): void {
|
||||
parent.querySelectorAll<HTMLAnchorElement>("a[href]:not(.internal-link)").forEach((link) => {
|
||||
const href = link.getAttribute("href") ?? "";
|
||||
const target = vaultFileLinkTarget(context.app, context.vaultPath, href);
|
||||
if (!target) return;
|
||||
|
||||
link.addClass("codex-panel__filelink");
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
void context.app.workspace.openLinkText(target, sourcePath, false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -2,51 +2,60 @@ import type { App, Component } from "obsidian";
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import { copyTextWithNotice } from "../../../../shared/ui/clipboard";
|
||||
import type { ChatAction, ChatState, ChatStateStore } from "../../state/reducer";
|
||||
import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "../../state/reducer";
|
||||
import type { ComposerBoundaryScrollAction } from "../../conversation/composer/boundary-scroll";
|
||||
import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../message-virtualizer";
|
||||
import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "./virtualizer";
|
||||
import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./ports";
|
||||
import { createMessageStreamContextPort } from "./ports";
|
||||
import { MarkdownMessageRenderer } from "./markdown-renderer";
|
||||
import { messageStreamBlocksNode, type MessageStreamRenderState } from "./render";
|
||||
import { createMessageStreamRenderState } from "./render-model";
|
||||
import { messageStreamViewportNode, type MessageStreamViewportState } from "./viewport";
|
||||
import type { DisplayItem } from "../../display/types";
|
||||
import { implementPlanCandidateFromState } from "../../state/selectors";
|
||||
import {
|
||||
forkCandidatesFromItems,
|
||||
isForkCandidateItem,
|
||||
isRollbackCandidateItem,
|
||||
rollbackCandidateFromItems,
|
||||
} from "../../display/item-actions";
|
||||
import { messageStreamBlocks } from "./stream-blocks";
|
||||
import type { MessageStreamContext } from "./context";
|
||||
|
||||
interface ChatMessageRendererObsidianPort {
|
||||
interface MessageStreamRendererObsidianPort {
|
||||
app: App;
|
||||
owner: Component;
|
||||
}
|
||||
|
||||
interface ChatMessageRendererStatePort {
|
||||
interface MessageStreamRendererStatePort {
|
||||
store: ChatStateStore;
|
||||
}
|
||||
|
||||
interface ChatMessageRendererWorkspacePort {
|
||||
interface MessageStreamRendererWorkspacePort {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
interface ChatMessageRendererScrollPort {
|
||||
interface MessageStreamRendererScrollPort {
|
||||
consumeIntent: () => MessageStreamScrollIntent;
|
||||
}
|
||||
|
||||
interface ChatMessageRendererHistoryPort {
|
||||
interface MessageStreamRendererHistoryPort {
|
||||
loadOlderTurns: () => void;
|
||||
}
|
||||
|
||||
export interface ChatMessageRendererOptions {
|
||||
obsidian: ChatMessageRendererObsidianPort;
|
||||
state: ChatMessageRendererStatePort;
|
||||
workspace: ChatMessageRendererWorkspacePort;
|
||||
scroll: ChatMessageRendererScrollPort;
|
||||
history: ChatMessageRendererHistoryPort;
|
||||
export interface MessageStreamRendererOptions {
|
||||
obsidian: MessageStreamRendererObsidianPort;
|
||||
state: MessageStreamRendererStatePort;
|
||||
workspace: MessageStreamRendererWorkspacePort;
|
||||
scroll: MessageStreamRendererScrollPort;
|
||||
history: MessageStreamRendererHistoryPort;
|
||||
actions: ChatMessageStreamActionPort;
|
||||
requests: ChatMessageStreamRequestPort;
|
||||
}
|
||||
|
||||
export class ChatMessageRenderer {
|
||||
export class MessageStreamRenderer {
|
||||
private messageVirtualizer: MessageStreamVirtualizerHandle | null = null;
|
||||
private readonly markdownRenderer: MarkdownMessageRenderer;
|
||||
|
||||
constructor(private readonly options: ChatMessageRendererOptions) {
|
||||
constructor(private readonly options: MessageStreamRendererOptions) {
|
||||
this.markdownRenderer = new MarkdownMessageRenderer({
|
||||
app: options.obsidian.app,
|
||||
owner: options.obsidian.owner,
|
||||
|
|
@ -64,16 +73,15 @@ export class ChatMessageRenderer {
|
|||
|
||||
renderNode(): UiNode {
|
||||
const state = this.state;
|
||||
return messageStreamBlocksNode(this.renderStateFor(state));
|
||||
return messageStreamViewportNode(this.renderStateFor(state));
|
||||
}
|
||||
|
||||
private renderStateFor(state: ChatState): MessageStreamRenderState {
|
||||
return createMessageStreamRenderState({
|
||||
state,
|
||||
contextPort: this.messageStreamPort(),
|
||||
private renderStateFor(state: ChatState): MessageStreamViewportState {
|
||||
return {
|
||||
blocks: messageStreamBlocks(this.messageStreamContext(state, this.messageStreamPort())),
|
||||
consumeScrollIntent: this.options.scroll.consumeIntent,
|
||||
registerVirtualizer: this.registerVirtualizer,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private messageStreamPort(): ChatMessageStreamContextPort {
|
||||
|
|
@ -95,6 +103,51 @@ export class ChatMessageRenderer {
|
|||
});
|
||||
}
|
||||
|
||||
private messageStreamContext(state: ChatState, port: ChatMessageStreamContextPort): MessageStreamContext {
|
||||
const busy = chatTurnBusy(state);
|
||||
const rollbackCandidate = busy ? null : rollbackCandidateFromItems(state.messageStream.displayItems);
|
||||
const forkCandidates = busy ? [] : forkCandidatesFromItems(state.messageStream.displayItems);
|
||||
const implementPlanCandidate = implementPlanCandidateFromState(state);
|
||||
|
||||
return {
|
||||
activeThreadId: state.activeThread.id,
|
||||
turnLifecycle: state.turn.lifecycle,
|
||||
historyCursor: state.messageStream.historyCursor,
|
||||
loadingHistory: state.messageStream.loadingHistory,
|
||||
displayItems: state.messageStream.displayItems,
|
||||
turnDiffs: state.messageStream.turnDiffs,
|
||||
workspaceRoot: state.activeThread.cwd ?? port.vaultPath,
|
||||
openDetails: state.ui.openDetails,
|
||||
onDetailsToggle: port.setOpenDetail,
|
||||
loadOlderTurns: port.loadOlderTurns,
|
||||
renderMarkdown: port.renderMarkdown,
|
||||
copyText: port.copyMessageText,
|
||||
canImplementPlanItem: (item: DisplayItem) => item.id === implementPlanCandidate?.id,
|
||||
onImplementPlanItem: (item) => {
|
||||
port.actions.implementPlan(item);
|
||||
},
|
||||
canRollbackItem: (item: DisplayItem) => isRollbackCandidateItem(item, rollbackCandidate),
|
||||
onRollbackItem: () => {
|
||||
if (state.activeThread.id) port.actions.rollbackThread(state.activeThread.id);
|
||||
},
|
||||
canForkItem: (item: DisplayItem) => isForkCandidateItem(item, forkCandidates),
|
||||
onForkItem: (item, archiveSource) => {
|
||||
if (state.activeThread.id && item.turnId) {
|
||||
port.actions.forkThreadFromTurn(state.activeThread.id, item.turnId, archiveSource);
|
||||
}
|
||||
},
|
||||
openTurnDiff: (turnDiffState) => {
|
||||
port.actions.openTurnDiff(turnDiffState);
|
||||
},
|
||||
pendingRequests: {
|
||||
signature: port.requests.pendingSignature(),
|
||||
snapshot: port.requests.pendingSnapshot,
|
||||
actions: port.requests.pendingActions,
|
||||
consumeAutoFocus: port.requests.consumePendingAutoFocus,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.messageVirtualizer = null;
|
||||
}
|
||||
|
|
@ -107,11 +160,11 @@ export class ChatMessageRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
forceMessagesToBottom(): void {
|
||||
forceMessageStreamToBottom(): void {
|
||||
this.messageVirtualizer?.pinToBottom();
|
||||
}
|
||||
|
||||
repinMessagesToBottomIfPinned(): void {
|
||||
repinMessageStreamToBottomIfPinned(): void {
|
||||
this.messageVirtualizer?.repinToBottomIfPinned();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MessageStreamScrollIntent } from "../ui/message-virtualizer";
|
||||
import type { MessageStreamScrollIntent } from "./virtualizer";
|
||||
|
||||
export class ChatMessageScrollIntentController {
|
||||
private nextIntent: MessageStreamScrollIntent = "auto";
|
||||
|
|
@ -3,12 +3,11 @@ import { useLayoutEffect, useState } from "preact/hooks";
|
|||
|
||||
import { activeTurnId } from "../../state/reducer";
|
||||
import { displayBlocksForItems } from "../../display/stream/blocks";
|
||||
import type { ToolResultDisplayItem } from "../tool-result-view";
|
||||
import type { ToolResultDisplayItem } from "./tool-result-view-model";
|
||||
import type { DisplayBlock, DisplayItem } from "../../display/types";
|
||||
import { userInputDraftKey, userInputOtherDraftKey } from "../../protocol/server-requests/user-input";
|
||||
import { pendingRequestBlockNode } from "../pending-request-block";
|
||||
import { toolResultNode } from "../tool-result";
|
||||
import { activeAgentRunSummaryBlock, agentRunSummaryNode, workItemNode, type WorkItemDisplayItem } from "../work-items";
|
||||
import { pendingRequestBlockNode } from "./pending-request-block";
|
||||
import { toolResultNode } from "./tool-result";
|
||||
import { activeAgentRunSummaryBlock, agentRunSummaryNode, workItemNode, type WorkItemDisplayItem } from "./work-items";
|
||||
import type { MessageStreamBlock, MessageStreamContext, TextDisplayItem } from "./context";
|
||||
import { textItemNode } from "./text-item";
|
||||
|
||||
|
|
@ -92,11 +91,7 @@ function bottomLiveBlocks(context: MessageStreamContext, activeTurn: string | nu
|
|||
node: pendingRequestBlockNode(
|
||||
snapshot.approvals,
|
||||
snapshot.pendingUserInputs,
|
||||
{
|
||||
values: snapshot.userInputDrafts,
|
||||
draftKey: userInputDraftKey,
|
||||
otherDraftKey: userInputOtherDraftKey,
|
||||
},
|
||||
snapshot.userInputDrafts,
|
||||
snapshot.openDetails,
|
||||
context.pendingRequests.actions(),
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { type ComponentChild as UiNode, type Ref } from "preact";
|
|||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { DisplayItem, ExecutionState } from "../../display/types";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../message-content-events";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "./content-events";
|
||||
import type { TextItemContentContext, TextItemContext, TextDisplayItem } from "./context";
|
||||
import { TextItemHeader } from "./text-item-actions";
|
||||
import { AutoReviewSummaries, EditedFiles, MentionedFiles, TextItemDetails, ReferencedThread, SystemDetails } from "./text-item-metadata";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { pathRelativeToRoot } from "../display/details/path-labels";
|
||||
import { definedProp } from "../../../utils";
|
||||
import { pathRelativeToRoot } from "../../display/details/path-labels";
|
||||
import { definedProp } from "../../../../utils";
|
||||
import type {
|
||||
ApprovalResultDisplayItem,
|
||||
CommandDisplayItem,
|
||||
|
|
@ -12,7 +12,7 @@ import type {
|
|||
HookDisplayItem,
|
||||
ReviewResultDisplayItem,
|
||||
ToolCallDisplayItem,
|
||||
} from "../display/types";
|
||||
} from "../../display/types";
|
||||
|
||||
export type ToolResultDisplayItem =
|
||||
| CommandDisplayItem
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { toolResultView, type ToolResultDetailSection, type ToolResultDisplayItem, type ToolResultView } from "./tool-result-view";
|
||||
import { renderRawDiffLines } from "../../../shared/diff/render";
|
||||
import { toolResultView, type ToolResultDetailSection, type ToolResultDisplayItem, type ToolResultView } from "./tool-result-view-model";
|
||||
import { renderRawDiffLines } from "../../../../shared/diff/render";
|
||||
|
||||
export interface ToolResultRenderContext {
|
||||
workspaceRoot?: string | null;
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { type MessageStreamScrollIntent, type MessageStreamVirtualizerHandle, useMessageStreamVirtualizer } from "../message-virtualizer";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../message-content-events";
|
||||
import { type MessageStreamScrollIntent, type MessageStreamVirtualizerHandle, useMessageStreamVirtualizer } from "./virtualizer";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "./content-events";
|
||||
import type { MessageStreamBlock } from "./context";
|
||||
|
||||
const MESSAGE_BLOCK_ESTIMATE_SIZE = 96;
|
||||
const MESSAGE_STREAM_INITIAL_RENDER_LIMIT = 32;
|
||||
|
||||
export interface MessageStreamRenderState {
|
||||
export interface MessageStreamViewportState {
|
||||
blocks: MessageStreamBlock[];
|
||||
consumeScrollIntent: () => MessageStreamScrollIntent;
|
||||
registerVirtualizer?: (virtualizer: MessageStreamVirtualizerHandle) => () => void;
|
||||
}
|
||||
|
||||
export function messageStreamBlocksNode(state: MessageStreamRenderState): UiNode {
|
||||
return <MessageStreamBlocks state={state} />;
|
||||
export function messageStreamViewportNode(state: MessageStreamViewportState): UiNode {
|
||||
return <MessageStreamViewport state={state} />;
|
||||
}
|
||||
|
||||
function MessageStreamBlocks({ state }: { state: MessageStreamRenderState }): UiNode {
|
||||
function MessageStreamViewport({ state }: { state: MessageStreamViewportState }): UiNode {
|
||||
const { blocks, consumeScrollIntent, registerVirtualizer } = state;
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const virtualizer = useMessageStreamVirtualizer({ blocks, consumeScrollIntent, registerVirtualizer, scrollElementRef });
|
||||
|
|
@ -31,7 +31,7 @@ function MessageStreamBlocks({ state }: { state: MessageStreamRenderState }): Ui
|
|||
);
|
||||
|
||||
return (
|
||||
<div ref={scrollElementRef} className="codex-panel__region codex-panel__region--messages codex-panel__messages">
|
||||
<div ref={scrollElementRef} className="codex-panel__region codex-panel__region--message-stream codex-panel__messages">
|
||||
<div className="codex-panel__message-virtualizer" style={{ height: `${String(virtualizer.getTotalSize())}px` }}>
|
||||
{virtualItems.map((virtualItem) => (
|
||||
<MessageStreamBlockHost
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { elementScroll, observeElementOffset, observeElementRect, Virtualizer, type VirtualItem } from "@tanstack/virtual-core";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { MessageStreamBlock } from "./message-stream/context";
|
||||
import type { MessageStreamBlock } from "./context";
|
||||
|
||||
export type MessageStreamScrollIntent = "auto" | "force-bottom" | "follow-bottom" | "preserve";
|
||||
type MessageScrollDirection = -1 | 1;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { useLayoutEffect, useState } from "preact/hooks";
|
||||
|
||||
import { activeAgentRunSummary, agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel } from "../display/stream/agent-summary";
|
||||
import { activeAgentRunSummary, agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel } from "../../display/stream/agent-summary";
|
||||
import type {
|
||||
AgentDisplayItem,
|
||||
AgentRunSummary,
|
||||
|
|
@ -11,10 +11,10 @@ import type {
|
|||
ExecutionState,
|
||||
ReasoningDisplayItem,
|
||||
TaskProgressDisplayItem,
|
||||
} from "../display/types";
|
||||
import { activeTurnId, type ChatTurnLifecycleState } from "../state/reducer";
|
||||
} from "../../display/types";
|
||||
import { activeTurnId, type ChatTurnLifecycleState } from "../../state/reducer";
|
||||
import { createWorkMessageClassName } from "./work-message";
|
||||
import { shortThreadId, truncate } from "../../../utils";
|
||||
import { shortThreadId, truncate } from "../../../../utils";
|
||||
|
||||
const AGENT_ROW_MESSAGE_PREVIEW_LIMIT = 120;
|
||||
const AGENT_ACTIVITY_PROMPT_PREVIEW_LIMIT = 96;
|
||||
|
|
@ -23,7 +23,7 @@ export interface ChatPanelShellProps {
|
|||
showToolbar: boolean;
|
||||
toolbarNode: () => UiNode;
|
||||
goalNode: () => UiNode;
|
||||
messagesNode: () => UiNode;
|
||||
messageStreamNode: () => UiNode;
|
||||
composerNode: () => UiNode;
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ function ChatPanelShell({
|
|||
showToolbar,
|
||||
toolbarNode,
|
||||
goalNode,
|
||||
messagesNode,
|
||||
messageStreamNode,
|
||||
composerNode,
|
||||
shellState,
|
||||
}: ChatPanelShellProps & { shellState: ChatPanelShellState }): UiNode {
|
||||
|
|
@ -139,7 +139,7 @@ function ChatPanelShell({
|
|||
{showToolbar ? <div className="codex-panel__toolbar">{toolbarNode()}</div> : null}
|
||||
<div className="codex-panel__body">
|
||||
<div className="codex-panel__region codex-panel__region--goal">{goalNode()}</div>
|
||||
{messagesNode()}
|
||||
{messageStreamNode()}
|
||||
<div className="codex-panel__region codex-panel__region--composer">{composerNode()}</div>
|
||||
</div>
|
||||
</ChatPanelShellStateContext.Provider>
|
||||
|
|
|
|||
|
|
@ -23,16 +23,16 @@ import { codexPanelDisplayTitle, getThreadTitle } from "../../domain/threads/mod
|
|||
import { connectionDiagnosticsModel } from "./panel/regions/toolbar";
|
||||
import { openPanelTurnLifecycle } from "./panel/snapshot";
|
||||
import { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "./lifecycle";
|
||||
import { ChatMessageScrollIntentController } from "./ui/message-scroll-intent-controller";
|
||||
import { ChatMessageScrollIntentController } from "./ui/message-stream/scroll-intent-controller";
|
||||
import type { ChatControllerCompositionPorts } from "./composition-ports";
|
||||
import { createChatViewControllers, type ChatViewControllers } from "./composition";
|
||||
import type { ChatPanelComposerPorts, ChatPanelGoalPorts, ChatPanelMessagesPorts, ChatPanelToolbarPorts } from "./panel/regions/ports";
|
||||
import type { ChatPanelComposerPorts, ChatPanelGoalPorts, ChatPanelMessageStreamPorts, ChatPanelToolbarPorts } from "./panel/regions/ports";
|
||||
import { chatPanelComposerMetaViewModel, chatPanelComposerPlaceholder } from "./panel/regions/composer";
|
||||
import { chatPanelPendingRequestsSignature } from "./panel/regions/messages";
|
||||
import { chatPanelMessageStreamPendingRequestsSignature } from "./panel/regions/message-stream";
|
||||
import {
|
||||
chatPanelComposerRegionNode,
|
||||
chatPanelGoalRegionNode,
|
||||
chatPanelMessagesRegionNode,
|
||||
chatPanelMessageStreamRegionNode,
|
||||
chatPanelToolbarRegionNode,
|
||||
} from "./panel/regions/render";
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export class CodexChatView extends ItemView {
|
|||
private readonly messageScrollIntent: ChatMessageScrollIntentController;
|
||||
private readonly toolbarPorts: ChatPanelToolbarPorts;
|
||||
private readonly goalPorts: ChatPanelGoalPorts;
|
||||
private readonly messagesPorts: ChatPanelMessagesPorts;
|
||||
private readonly messageStreamPorts: ChatPanelMessageStreamPorts;
|
||||
private readonly composerPorts: ChatPanelComposerPorts;
|
||||
private readonly connectionWork = new ChatConnectionWorkTracker();
|
||||
private readonly resumeWork: ChatResumeWorkTracker;
|
||||
|
|
@ -68,7 +68,7 @@ export class CodexChatView extends ItemView {
|
|||
const panelPorts = this.createPanelRegionPorts(this.controllers);
|
||||
this.toolbarPorts = panelPorts.toolbar;
|
||||
this.goalPorts = panelPorts.goal;
|
||||
this.messagesPorts = panelPorts.messages;
|
||||
this.messageStreamPorts = panelPorts.messageStream;
|
||||
this.composerPorts = panelPorts.composer;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ export class CodexChatView extends ItemView {
|
|||
panelRoot: () => this.panelRoot(),
|
||||
toolbarNode: () => chatPanelToolbarRegionNode(this.toolbarPorts),
|
||||
goalNode: () => chatPanelGoalRegionNode(this.goalPorts),
|
||||
messagesNode: () => chatPanelMessagesRegionNode(this.messagesPorts),
|
||||
messageStreamNode: () => chatPanelMessageStreamRegionNode(this.messageStreamPorts),
|
||||
composerNode: () => chatPanelComposerRegionNode(() => this.controllers.composer.controller.renderNode()),
|
||||
closeToolbarPanelOnOutsidePointer: (event) => {
|
||||
this.closeToolbarPanelOnOutsidePointer(event);
|
||||
|
|
@ -176,8 +176,8 @@ export class CodexChatView extends ItemView {
|
|||
this.scheduleRender();
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
pendingRequestsSignature: () => chatPanelPendingRequestsSignature(this.messagesPorts),
|
||||
messageStream: {
|
||||
pendingRequestsSignature: () => chatPanelMessageStreamPendingRequestsSignature(this.messageStreamPorts),
|
||||
},
|
||||
composerView: {
|
||||
composerPlaceholder: () => chatPanelComposerPlaceholder(this.composerPorts),
|
||||
|
|
@ -234,7 +234,7 @@ export class CodexChatView extends ItemView {
|
|||
private createPanelRegionPorts(controllers: ChatViewControllers): {
|
||||
toolbar: ChatPanelToolbarPorts;
|
||||
goal: ChatPanelGoalPorts;
|
||||
messages: ChatPanelMessagesPorts;
|
||||
messageStream: ChatPanelMessageStreamPorts;
|
||||
composer: ChatPanelComposerPorts;
|
||||
} {
|
||||
const state = {
|
||||
|
|
@ -271,10 +271,10 @@ export class CodexChatView extends ItemView {
|
|||
goal: this.createGoalPanelActions(controllers),
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
messageStream: {
|
||||
state,
|
||||
render: {
|
||||
node: () => this.controllers.render.messages.renderNode(),
|
||||
node: () => this.controllers.render.messageStream.renderNode(),
|
||||
},
|
||||
},
|
||||
composer: {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
grid-row: 1;
|
||||
}
|
||||
|
||||
.codex-panel__region--messages {
|
||||
.codex-panel__region--message-stream {
|
||||
grid-row: 2;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ describe("PendingRequestController", () => {
|
|||
stateStore.dispatch({ type: "request/user-input-queued", input });
|
||||
stateStore.dispatch({ type: "request/user-input-draft-set", key: "7:direction", value: "Left" });
|
||||
|
||||
pendingRequests.resolveUserInput(input);
|
||||
pendingRequests.resolveUserInput(input.requestId);
|
||||
|
||||
expect(respondToServerRequest).toHaveBeenCalledWith(7, { answers: { direction: { answers: ["Left"] } } });
|
||||
expect(stateStore.getState().requests.pendingUserInputs).toEqual([]);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { PendingRequestBlockSnapshot } from "../../../../../src/features/chat/conversation/pending-requests/snapshot";
|
||||
import { pendingApprovalViewModel } from "../../../../../src/features/chat/conversation/pending-requests/view-model";
|
||||
import type { PendingApproval } from "../../../../../src/features/chat/protocol/server-requests/approval";
|
||||
import type { PendingUserInput } from "../../../../../src/features/chat/protocol/server-requests/user-input";
|
||||
import type { PendingRequestBlockContext } from "../../../../../src/features/chat/ui/message-stream/context";
|
||||
|
|
@ -60,7 +61,7 @@ describe("pending request renderer decisions", () => {
|
|||
actEvent(() => {
|
||||
parent.querySelector<HTMLButtonElement>(".mod-cta")?.click();
|
||||
});
|
||||
expect(resolveUserInput).toHaveBeenCalledWith(input);
|
||||
expect(resolveUserInput).toHaveBeenCalledWith(input.requestId);
|
||||
});
|
||||
|
||||
it("selects the other Plan mode answer from controlled drafts", () => {
|
||||
|
|
@ -349,7 +350,7 @@ describe("pending request renderer decisions", () => {
|
|||
actEvent(() => {
|
||||
allowButton.click();
|
||||
});
|
||||
expect(resolveApproval).toHaveBeenCalledWith(approval, {
|
||||
expect(resolveApproval).toHaveBeenCalledWith(approval.requestId, {
|
||||
kind: "command-decision",
|
||||
decision: { applyNetworkPolicyAmendment: { network_policy_amendment: { host: "registry.npmjs.org", action: "allow" } } },
|
||||
});
|
||||
|
|
@ -527,7 +528,7 @@ describe("pending request renderer decisions", () => {
|
|||
renderMarkdown: (element, text) => element.createDiv({ text }),
|
||||
pendingRequests: pendingRequestContext({
|
||||
signature: "approval:1",
|
||||
snapshot: emptyPendingRequestBlockSnapshot({ approvals: [pendingApproval()] }),
|
||||
snapshot: emptyPendingRequestBlockSnapshot({ approvals: [pendingApprovalViewModel(pendingApproval())] }),
|
||||
consumeAutoFocus,
|
||||
}),
|
||||
}),
|
||||
|
|
@ -587,7 +588,7 @@ describe("pending request renderer decisions", () => {
|
|||
...baseContext,
|
||||
pendingRequests: pendingRequestContext({
|
||||
signature: "request:1",
|
||||
snapshot: emptyPendingRequestBlockSnapshot({ approvals: [pendingApproval()] }),
|
||||
snapshot: emptyPendingRequestBlockSnapshot({ approvals: [pendingApprovalViewModel(pendingApproval())] }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import {
|
|||
type ChatState,
|
||||
type ChatStateStore,
|
||||
} from "../../../../../src/features/chat/state/reducer";
|
||||
import { ChatMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/renderer";
|
||||
import { MessageStreamRenderer } from "../../../../../src/features/chat/ui/message-stream/renderer";
|
||||
import {
|
||||
bindRenderedWikiLinks,
|
||||
type RenderedMarkdownLinkContext,
|
||||
} from "../../../../../src/features/chat/ui/message-stream/rendered-markdown-links";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-content-events";
|
||||
import type { MessageStreamScrollIntent } from "../../../../../src/features/chat/ui/message-virtualizer";
|
||||
} from "../../../../../src/features/chat/ui/message-stream/markdown-renderer";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events";
|
||||
import type { MessageStreamScrollIntent } from "../../../../../src/features/chat/ui/message-stream/virtualizer";
|
||||
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root";
|
||||
import { notices } from "../../../../mocks/obsidian";
|
||||
import { installObsidianDomShims } from "../../../../support/dom";
|
||||
|
|
@ -26,7 +26,7 @@ const ESTIMATED_MESSAGE_BLOCK_HEIGHT = 96;
|
|||
|
||||
installObsidianDomShims();
|
||||
|
||||
describe("ChatMessageRenderer scroll pinning", () => {
|
||||
describe("MessageStreamRenderer scroll pinning", () => {
|
||||
beforeEach(() => {
|
||||
notices.length = 0;
|
||||
});
|
||||
|
|
@ -143,7 +143,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const messages = messageViewport(parent);
|
||||
|
|
@ -153,7 +153,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
|
||||
const scrollIntoView = vi.spyOn(HTMLElement.prototype, "scrollIntoView");
|
||||
scrollIntoView.mockClear();
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
await settleMessageRender(messages);
|
||||
|
||||
expect(messages.scrollTop).toBe(0);
|
||||
|
|
@ -175,7 +175,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const messages = messageViewport(parent);
|
||||
|
|
@ -187,7 +187,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
installMessageViewportMetrics(messages, { clientHeight: 100 });
|
||||
messages.scrollTop = 940;
|
||||
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
|
||||
expect(messages.scrollTop).toBe(0);
|
||||
});
|
||||
|
|
@ -207,7 +207,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
const messages = parent.createDiv({ cls: "codex-panel__messages" });
|
||||
let scrollTop = 0;
|
||||
|
|
@ -237,7 +237,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
await settleMessageRender(messages);
|
||||
expect(messages.scrollTop).toBe(0);
|
||||
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
expect(messages.scrollTop).toBe(0);
|
||||
|
||||
layoutSettled = true;
|
||||
|
|
@ -247,15 +247,15 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
});
|
||||
|
||||
it("treats scroll commands as no-ops when no message stream virtualizer is mounted", () => {
|
||||
const renderer = chatMessageRenderer();
|
||||
const renderer = messageStreamRenderer();
|
||||
|
||||
expect(() => {
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.repinMessagesToBottomIfPinned();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
renderer.repinMessageStreamToBottomIfPinned();
|
||||
renderer.scrollFromComposer({ direction: 1, amount: "text-lines" });
|
||||
renderer.scrollFromComposer({ direction: -1, amount: "page" });
|
||||
renderer.dispose();
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
|
|
@ -274,7 +274,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const messages = messageViewport(parent);
|
||||
installMessageViewportMetrics(messages);
|
||||
|
|
@ -283,7 +283,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
unmountUiRoot(parent);
|
||||
|
||||
expect(() => {
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
renderer.scrollFromComposer({ direction: 1, amount: "page" });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
|
@ -303,7 +303,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const oldMessages = messageViewport(parent);
|
||||
installMessageViewportMetrics(oldMessages, { clientHeight: 100, scrollHeight: 1000 });
|
||||
|
|
@ -316,7 +316,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
const newMessages = messageViewport(parent);
|
||||
installMessageViewportMetrics(newMessages, { clientHeight: 100, scrollHeight: 1000 });
|
||||
await settleMessageRender(newMessages);
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
|
||||
expect(newMessages.scrollTop).toBe(900);
|
||||
expect(oldMessages.scrollTop).toBe(125);
|
||||
|
|
@ -337,12 +337,12 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state, vi.fn(), "/vault", [], () => "force-bottom");
|
||||
const renderer = messageStreamRenderer(state, vi.fn(), "/vault", [], () => "force-bottom");
|
||||
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const messages = messageViewport(parent);
|
||||
installMessageViewportMetrics(messages, { clientHeight: 100, scrollHeight: 1000 });
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
await settleMessageRender(messages);
|
||||
|
||||
expect(messages.scrollTop).toBe(900);
|
||||
|
|
@ -363,7 +363,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const messages = messageViewport(parent);
|
||||
|
|
@ -374,7 +374,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
configurable: true,
|
||||
});
|
||||
|
||||
renderer.forceMessagesToBottom();
|
||||
renderer.forceMessageStreamToBottom();
|
||||
await settleMessageRender(messages);
|
||||
expect(messages.scrollTop).toBe(900);
|
||||
|
||||
|
|
@ -402,7 +402,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
const messages = parent.createDiv({ cls: "codex-panel__messages" });
|
||||
installMessageViewportMetrics(messages);
|
||||
|
|
@ -448,7 +448,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
const messages = parent.createDiv({ cls: "codex-panel__messages" });
|
||||
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
|
||||
|
|
@ -480,7 +480,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
},
|
||||
];
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
let messages = messageViewport(parent);
|
||||
|
|
@ -508,7 +508,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
messageState: "completed",
|
||||
}));
|
||||
const parent = document.createElement("div");
|
||||
const renderer = chatMessageRenderer(state);
|
||||
const renderer = messageStreamRenderer(state);
|
||||
|
||||
renderUiRoot(parent, renderer.renderNode());
|
||||
const messages = messageViewport(parent);
|
||||
|
|
@ -536,15 +536,15 @@ function markdownLinkContext(openLinkText = vi.fn(), vaultPath = "/vault", vault
|
|||
};
|
||||
}
|
||||
|
||||
function chatMessageRenderer(
|
||||
function messageStreamRenderer(
|
||||
state = createChatState(),
|
||||
openLinkText = vi.fn(),
|
||||
vaultPath = "/vault",
|
||||
vaultFiles: string[] = [],
|
||||
consumeIntent: () => MessageStreamScrollIntent = () => "auto",
|
||||
): ChatMessageRenderer {
|
||||
): MessageStreamRenderer {
|
||||
const files = new Map(vaultFiles.map((path) => [path, tFile(path)]));
|
||||
return new ChatMessageRenderer({
|
||||
return new MessageStreamRenderer({
|
||||
obsidian: {
|
||||
app: {
|
||||
workspace: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ChatMessageScrollIntentController } from "../../../../src/features/chat/ui/message-scroll-intent-controller";
|
||||
import { ChatMessageScrollIntentController } from "../../../../../src/features/chat/ui/message-stream/scroll-intent-controller";
|
||||
|
||||
describe("ChatMessageScrollIntentController", () => {
|
||||
it("consumes one-shot scroll intents", () => {
|
||||
|
|
@ -4,11 +4,13 @@ import { act } from "preact/test-utils";
|
|||
|
||||
import type { PendingApproval } from "../../../../../src/features/chat/protocol/server-requests/approval";
|
||||
import type { PendingUserInput } from "../../../../../src/features/chat/protocol/server-requests/user-input";
|
||||
import { pendingRequestBlockNode, type PendingRequestBlockActions } from "../../../../../src/features/chat/ui/pending-request-block";
|
||||
import { pendingRequestBlockSnapshotFromRequests } from "../../../../../src/features/chat/conversation/pending-requests/snapshot";
|
||||
import type { PendingRequestBlockActions } from "../../../../../src/features/chat/conversation/pending-requests/view-model";
|
||||
import { pendingRequestBlockNode } from "../../../../../src/features/chat/ui/message-stream/pending-request-block";
|
||||
import type { ChatTurnLifecycleState } from "../../../../../src/features/chat/state/reducer";
|
||||
import { messageStreamBlocks as rawMessageStreamBlocks } from "../../../../../src/features/chat/ui/message-stream/stream-blocks";
|
||||
import type { MessageStreamBlock } from "../../../../../src/features/chat/ui/message-stream/context";
|
||||
import { messageStreamBlocksNode } from "../../../../../src/features/chat/ui/message-stream/render";
|
||||
import { messageStreamViewportNode } from "../../../../../src/features/chat/ui/message-stream/viewport";
|
||||
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root";
|
||||
|
||||
export function messageStreamBlocks(
|
||||
|
|
@ -55,7 +57,7 @@ export function renderMessageStreamBlocksInAct(parent: HTMLElement, blocks: Mess
|
|||
void act(() => {
|
||||
renderUiRoot(
|
||||
parent,
|
||||
messageStreamBlocksNode({
|
||||
messageStreamViewportNode({
|
||||
blocks,
|
||||
consumeScrollIntent: () => "auto",
|
||||
}),
|
||||
|
|
@ -93,8 +95,40 @@ export function unmountUiRootInAct(parent: HTMLElement): void {
|
|||
});
|
||||
}
|
||||
|
||||
export function renderPendingRequestNode(parent: HTMLElement, ...args: Parameters<typeof pendingRequestBlockNode>): void {
|
||||
renderUiRootInAct(parent, pendingRequestBlockNode(...args));
|
||||
export function renderPendingRequestNode(
|
||||
parent: HTMLElement,
|
||||
approvals: readonly PendingApproval[],
|
||||
pendingUserInputs: readonly PendingUserInput[],
|
||||
drafts: {
|
||||
values: ReadonlyMap<string, string>;
|
||||
draftKey?: (requestId: PendingUserInput["requestId"], questionId: string) => string;
|
||||
otherDraftKey?: (requestId: PendingUserInput["requestId"], questionId: string) => string;
|
||||
},
|
||||
openDetails: ReadonlySet<string>,
|
||||
actions: PendingRequestBlockActions,
|
||||
autoFocusRequested = false,
|
||||
consumeAutoFocus?: () => boolean,
|
||||
autoFocusSignature = "",
|
||||
): void {
|
||||
const snapshot = pendingRequestBlockSnapshotFromRequests({
|
||||
approvals,
|
||||
pendingUserInputs,
|
||||
userInputDrafts: drafts.values,
|
||||
openDetails,
|
||||
});
|
||||
renderUiRootInAct(
|
||||
parent,
|
||||
pendingRequestBlockNode(
|
||||
snapshot.approvals,
|
||||
snapshot.pendingUserInputs,
|
||||
snapshot.userInputDrafts,
|
||||
snapshot.openDetails,
|
||||
actions,
|
||||
autoFocusRequested,
|
||||
consumeAutoFocus,
|
||||
autoFocusSignature,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function pendingRequestActions(overrides: Partial<PendingRequestBlockActions> = {}): PendingRequestBlockActions {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { messageStreamVirtualItems } from "../../../../../src/features/chat/ui/message-stream/render";
|
||||
import { messageStreamVirtualItems } from "../../../../../src/features/chat/ui/message-stream/viewport";
|
||||
import type { MessageStreamBlock } from "../../../../../src/features/chat/ui/message-stream/context";
|
||||
|
||||
describe("message stream virtual item fallback", () => {
|
||||
|
|
@ -9,10 +9,10 @@ import {
|
|||
type MessageStreamVirtualizerHandle,
|
||||
type MessageStreamVirtualizerView,
|
||||
useMessageStreamVirtualizer,
|
||||
} from "../../../../src/features/chat/ui/message-virtualizer";
|
||||
import type { MessageStreamBlock } from "../../../../src/features/chat/ui/message-stream/context";
|
||||
import { renderUiRoot, unmountUiRoot } from "../../../../src/shared/ui/ui-root";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
} from "../../../../../src/features/chat/ui/message-stream/virtualizer";
|
||||
import type { MessageStreamBlock } from "../../../../../src/features/chat/ui/message-stream/context";
|
||||
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root";
|
||||
import { installObsidianDomShims } from "../../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import { act } from "preact/test-utils";
|
||||
|
||||
import type { ThreadGoal } from "../../../../../src/app-server/protocol/thread-goal";
|
||||
import { goalBannerNode, type GoalBannerActions } from "../../../../../src/features/chat/ui/goal-banner";
|
||||
import { goalRegionNode, type GoalRegionActions } from "../../../../../src/features/chat/ui/goal";
|
||||
import type { SendShortcut } from "../../../../../src/shared/ui/keyboard";
|
||||
import { renderUiRoot } from "../../../../../src/shared/ui/ui-root";
|
||||
import { installObsidianDomShims } from "../../../../support/dom";
|
||||
|
|
@ -12,7 +12,7 @@ import { installObsidianDomShims } from "../../../../support/dom";
|
|||
installObsidianDomShims();
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe("goalBannerNode", () => {
|
||||
describe("goalRegionNode", () => {
|
||||
it("renders nothing when there is no goal", async () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ describe("goalBannerNode", () => {
|
|||
const onEditingChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderUiRoot(parent, goalBannerNode(null, callbacks, { sendShortcut: "enter", editingRequested: true, onEditingChange }));
|
||||
renderUiRoot(parent, goalRegionNode(null, callbacks, { sendShortcut: "enter", editingRequested: true, onEditingChange }));
|
||||
});
|
||||
|
||||
expect(parent.textContent).toContain("Goal");
|
||||
|
|
@ -234,10 +234,10 @@ describe("goalBannerNode", () => {
|
|||
function renderGoal(
|
||||
parent: HTMLElement,
|
||||
currentGoal: ThreadGoal | null,
|
||||
callbacks: GoalBannerActions = actions(),
|
||||
callbacks: GoalRegionActions = actions(),
|
||||
sendShortcut: SendShortcut = "enter",
|
||||
): void {
|
||||
renderUiRoot(parent, goalBannerNode(currentGoal, callbacks, { sendShortcut }));
|
||||
renderUiRoot(parent, goalRegionNode(currentGoal, callbacks, { sendShortcut }));
|
||||
}
|
||||
|
||||
function actions() {
|
||||
|
|
@ -28,7 +28,7 @@ describe("ChatPanelShell", () => {
|
|||
expect(container.textContent).toContain("0");
|
||||
expect(container.textContent).toContain("ready");
|
||||
expect(container.querySelector(".codex-panel__region--config")).toBeNull();
|
||||
expect(container.querySelector(".codex-panel__body > .codex-panel__region--messages")).toBe(
|
||||
expect(container.querySelector(".codex-panel__body > .codex-panel__region--message-stream")).toBe(
|
||||
container.querySelector(".codex-panel__body > .codex-panel__messages"),
|
||||
);
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ describe("ChatPanelShell", () => {
|
|||
|
||||
expect(container.querySelector(".codex-panel__toolbar .test-toolbar")?.textContent).toBe("Working");
|
||||
expect(container.querySelector(".codex-panel__region--goal .test-goal")?.textContent).toBe("no goal");
|
||||
expect(container.querySelector(".codex-panel__region--messages .test-messages")?.textContent).toBe("1");
|
||||
expect(container.querySelector(".codex-panel__region--message-stream .test-messages")?.textContent).toBe("1");
|
||||
expect(container.querySelector<HTMLTextAreaElement>(".codex-panel__region--composer .test-composer textarea")?.value).toBe("ready");
|
||||
expect(container.querySelector(".codex-panel__region--composer .test-toolbar")).toBeNull();
|
||||
expect(container.querySelector(".codex-panel__region--composer .test-messages")).toBeNull();
|
||||
|
|
@ -106,7 +106,7 @@ describe("ChatPanelShell", () => {
|
|||
});
|
||||
|
||||
expect(container.querySelector(".codex-panel__toolbar .test-toolbar")?.textContent).toBe("Idle");
|
||||
expect(container.querySelector(".codex-panel__region--messages .test-messages")?.textContent).toBe("0");
|
||||
expect(container.querySelector(".codex-panel__region--message-stream .test-messages")?.textContent).toBe("0");
|
||||
|
||||
await act(async () => {
|
||||
unmountChatPanelShell(container);
|
||||
|
|
@ -132,7 +132,7 @@ describe("ChatPanelShell", () => {
|
|||
});
|
||||
renderers.toolbarNode.mockClear();
|
||||
renderers.goalNode.mockClear();
|
||||
renderers.messagesNode.mockClear();
|
||||
renderers.messageStreamNode.mockClear();
|
||||
renderers.composerNode.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -143,7 +143,7 @@ describe("ChatPanelShell", () => {
|
|||
expect(container.querySelector(".test-toolbar")?.textContent).toBe("Working");
|
||||
expect(renderers.toolbarNode).not.toHaveBeenCalled();
|
||||
expect(renderers.goalNode).not.toHaveBeenCalled();
|
||||
expect(renderers.messagesNode).not.toHaveBeenCalled();
|
||||
expect(renderers.messageStreamNode).not.toHaveBeenCalled();
|
||||
expect(renderers.composerNode).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -163,7 +163,7 @@ describe("ChatPanelShell", () => {
|
|||
});
|
||||
|
||||
expect(container.querySelector(".codex-panel__toolbar")).toBeNull();
|
||||
expect(container.querySelector(".codex-panel__region--messages")).not.toBeNull();
|
||||
expect(container.querySelector(".codex-panel__region--message-stream")).not.toBeNull();
|
||||
expect(container.querySelector(".codex-panel__region--composer")).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -359,8 +359,8 @@ function shellRenderers(store: ReturnType<typeof createChatStateStore>) {
|
|||
|
||||
goalNode: vi.fn(() => <ShellGoal />),
|
||||
|
||||
messagesNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--messages codex-panel__messages">
|
||||
messageStreamNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--message-stream codex-panel__messages">
|
||||
<ShellMessageCount />
|
||||
</div>
|
||||
)),
|
||||
|
|
@ -382,8 +382,8 @@ function nestedRootShellRenderers(store: ReturnType<typeof createChatStateStore>
|
|||
|
||||
goalNode: vi.fn(() => <ShellGoal className="test-goal" />),
|
||||
|
||||
messagesNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--messages codex-panel__messages">
|
||||
messageStreamNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--message-stream codex-panel__messages">
|
||||
<ShellMessageCount className="test-messages" />
|
||||
</div>
|
||||
)),
|
||||
|
|
@ -405,8 +405,8 @@ function trackedRootShellRenderers(store: ReturnType<typeof createChatStateStore
|
|||
|
||||
goalNode: vi.fn(() => <TrackedSlot region="goal" cleanup={cleanup} />),
|
||||
|
||||
messagesNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--messages codex-panel__messages">
|
||||
messageStreamNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--message-stream codex-panel__messages">
|
||||
<TrackedSlot region="messages" cleanup={cleanup} />
|
||||
</div>
|
||||
)),
|
||||
|
|
@ -423,8 +423,8 @@ function nodeShellRenderers(store: ReturnType<typeof createChatStateStore>, clea
|
|||
|
||||
goalNode: vi.fn(() => <TrackedStateSlot region="goal" cleanup={cleanup} className="test-goal" selector="goal" />),
|
||||
|
||||
messagesNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--messages codex-panel__messages">
|
||||
messageStreamNode: vi.fn(() => (
|
||||
<div className="codex-panel__region codex-panel__region--message-stream codex-panel__messages">
|
||||
<TrackedStateSlot region="messages" cleanup={cleanup} className="test-messages" selector="message-count" />
|
||||
</div>
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe("chat toolbar archive confirmation signal", () => {
|
|||
showToolbar: true,
|
||||
toolbarNode: () => chatPanelToolbarRegionNode(toolbarPorts(store, controller)),
|
||||
goalNode: () => null,
|
||||
messagesNode: () => <div className="codex-panel__region codex-panel__region--messages codex-panel__messages" />,
|
||||
messageStreamNode: () => <div className="codex-panel__region codex-panel__region--message-stream codex-panel__messages" />,
|
||||
composerNode: () => null,
|
||||
});
|
||||
await settle();
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
const root = view.contentEl;
|
||||
expect(root.classList.contains("codex-panel")).toBe(true);
|
||||
expect(root.querySelector(":scope > .codex-panel__toolbar")).not.toBeNull();
|
||||
expect(root.querySelector(":scope > .codex-panel__body .codex-panel__region--messages")).not.toBeNull();
|
||||
expect(root.querySelector(":scope > .codex-panel__body .codex-panel__region--message-stream")).not.toBeNull();
|
||||
expect(root.querySelector(":scope > .codex-panel__body .codex-panel__region--composer")).not.toBeNull();
|
||||
expect(siblingRoots.every((sibling) => !sibling.classList.contains("codex-panel") && sibling.childElementCount === 0)).toBe(true);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue