mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Centralize chat domain derivations
This commit is contained in:
parent
28857413b3
commit
2992621354
21 changed files with 102 additions and 106 deletions
|
|
@ -147,6 +147,15 @@ export function approvalActionKind(action: ApprovalAction): "accept" | "accept-s
|
|||
return typeof action === "object" ? action.intent : action;
|
||||
}
|
||||
|
||||
export function defaultPendingApprovalOptions(): PendingApprovalOption[] {
|
||||
return [
|
||||
{ id: "accept", label: "Allow", action: "accept", intent: "accept" },
|
||||
{ id: "accept-session", label: "Allow session", action: "accept-session", intent: "accept-session" },
|
||||
{ id: "decline", label: "Deny", action: "decline", intent: "decline" },
|
||||
{ id: "cancel", label: "Cancel", action: "cancel", intent: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
export function questionDefaultAnswer(question: PendingUserInputQuestion): string {
|
||||
return question.options?.[0]?.label ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ThreadConversationSummary } from "./transcript";
|
||||
import { truncate } from "../../utils";
|
||||
import { truncate } from "../../shared/text/preview";
|
||||
|
||||
const THREAD_TITLE_CONTEXT_MAX_CHARS = 4_000;
|
||||
const DEFAULT_CONTEXT_PAGE_LIMIT = 20;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { shortThreadId } from "../../utils";
|
||||
import { shortThreadId } from "../../shared/id/thread-id";
|
||||
import type { Thread } from "./model";
|
||||
|
||||
const MAX_ARCHIVED_THREAD_DISPLAY_TITLE_LENGTH = 96;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ThreadCatalogEvent } from "../../../../workspace/thread-catalog";
|
|||
import { threadActivationSnapshotFromAppServerResponse } from "../../../../app-server/threads";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { runtimeConfigOrDefault } from "../../domain/runtime/effective";
|
||||
import { serviceTierRequestForThreadStart } from "../../application/runtime/thread-settings-update";
|
||||
import { serviceTierRequestForThreadStart } from "../runtime/thread-settings-update";
|
||||
import { resumedThreadAction } from "../../application/state/actions";
|
||||
import { captureChatServerActionClientScope, type ChatServerActionHost } from "./host";
|
||||
import type { ChatState } from "../../application/state/root-reducer";
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import {
|
|||
type RuntimeServiceTierRequest,
|
||||
type RuntimeSettingsPatch,
|
||||
} from "../../../../domain/runtime/thread-settings";
|
||||
import { currentModel, currentReasoningEffort, fastRuntimeServiceTierRequestValue } from "../../domain/runtime/effective";
|
||||
import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { currentModel, currentReasoningEffort, fastRuntimeServiceTierRequestValue } from "../../domain/runtime/effective";
|
||||
import { effectiveCollaborationMode, type PendingRuntimeIntent } from "../../domain/runtime/intent";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
|
||||
type TurnCollaborationModeWarning = "missing-model";
|
||||
|
||||
|
|
@ -34,16 +34,6 @@ export function serviceTierRequestForThreadStart(snapshot: RuntimeSnapshot, conf
|
|||
return runtimeSettingsPatchValue(serviceTierTransportIntent(snapshot, config, "thread-start"));
|
||||
}
|
||||
|
||||
function requestedTurnCollaborationModeSettings(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): TurnCollaborationModeSettings {
|
||||
const model = currentModel(snapshot, config);
|
||||
const effort = currentReasoningEffort(snapshot, config);
|
||||
if (!model) return { collaborationMode: null, warning: "missing-model" };
|
||||
return {
|
||||
collaborationMode: runtimeCollaborationModeSettings(snapshot.selectedCollaborationMode, model, effort),
|
||||
warning: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function pendingRuntimeSettingsPatch(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): PendingRuntimeSettingsPatch {
|
||||
const update: RuntimeSettingsPatch = {};
|
||||
const runtimeCollaborationModeSettings = requestedTurnCollaborationModeSettings(snapshot, config);
|
||||
|
|
@ -79,6 +69,16 @@ export function pendingRuntimeSettingsPatch(snapshot: RuntimeSnapshot, config: R
|
|||
return { update, collaborationModeWarning: null };
|
||||
}
|
||||
|
||||
function requestedTurnCollaborationModeSettings(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): TurnCollaborationModeSettings {
|
||||
const model = currentModel(snapshot, config);
|
||||
const effort = currentReasoningEffort(snapshot, config);
|
||||
if (!model) return { collaborationMode: null, warning: "missing-model" };
|
||||
return {
|
||||
collaborationMode: runtimeCollaborationModeSettings(snapshot.selectedCollaborationMode, model, effort),
|
||||
warning: null,
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeTransportIntentFromPending<T>(intent: PendingRuntimeIntent<T>): RuntimeTransportIntent<T> {
|
||||
if (intent.kind === "set") return { kind: "set", value: intent.value };
|
||||
if (intent.kind === "resetToConfig") return { kind: "clear" };
|
||||
|
|
@ -7,7 +7,7 @@ import { autoReviewActive, fastModeActive, runtimeConfigOrDefault } from "../../
|
|||
import {
|
||||
pendingRuntimeSettingsPatch as buildPendingRuntimeSettingsPatch,
|
||||
type PendingRuntimeSettingsPatch,
|
||||
} from "./thread-settings-update";
|
||||
} from "../../app-server/runtime/thread-settings-update";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { nextCollaborationMode, type CollaborationModeSelection, type RequestedFastMode } from "../../domain/runtime/intent";
|
||||
import type { ChatAction, ChatState } from "../state/root-reducer";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { upsertMessageStreamItemById } from "../../domain/message-stream/updates";
|
||||
import { completeReasoningItems, upsertMessageStreamItemById } from "../../domain/message-stream/updates";
|
||||
import type { MessageStreamItem, MessageStreamMessageItem } from "../../domain/message-stream/items";
|
||||
import { normalizeProposedPlanMarkdown } from "../../domain/message-stream/format/proposed-plan";
|
||||
import { messageStreamSemanticClassifications } from "../../domain/message-stream/semantics/classify";
|
||||
|
|
@ -402,44 +402,23 @@ function appendItemOutputToMessageStream(
|
|||
}
|
||||
|
||||
function completeReasoningInMessageStream(state: ChatMessageStreamState, turnId: string): ChatMessageStreamState {
|
||||
const stableUpdate = completedReasoningItems(state.stableItems, turnId);
|
||||
const stableItems = completeReasoningItems(state.stableItems, turnId);
|
||||
const activeSegment = state.activeSegment;
|
||||
|
||||
if (activeSegment?.turnId !== turnId) {
|
||||
return stableUpdate.changed ? patchObject(state, { stableItems: stableUpdate.items }) : state;
|
||||
return stableItems !== state.stableItems ? patchObject(state, { stableItems }) : state;
|
||||
}
|
||||
|
||||
const activeUpdate = completedReasoningItems(activeSegment.items, turnId);
|
||||
const activeItems = completeReasoningItems(activeSegment.items, turnId);
|
||||
|
||||
return stableUpdate.changed || activeUpdate.changed
|
||||
return stableItems !== state.stableItems || activeItems !== activeSegment.items
|
||||
? patchObject(state, {
|
||||
stableItems: stableUpdate.items,
|
||||
activeSegment: activeUpdate.changed ? activeSegmentFromItems(activeSegment.turnId, activeUpdate.items) : activeSegment,
|
||||
stableItems,
|
||||
activeSegment: activeItems !== activeSegment.items ? activeSegmentFromItems(activeSegment.turnId, activeItems) : activeSegment,
|
||||
})
|
||||
: state;
|
||||
}
|
||||
|
||||
function completedReasoningItems(
|
||||
items: readonly MessageStreamItem[],
|
||||
turnId: string,
|
||||
): { items: readonly MessageStreamItem[]; changed: boolean } {
|
||||
let changed = false;
|
||||
const nextItems: MessageStreamItem[] = [];
|
||||
for (const item of items) {
|
||||
if (item.kind !== "reasoning" || item.turnId !== turnId) {
|
||||
nextItems.push(item);
|
||||
} else {
|
||||
changed = true;
|
||||
nextItems.push({
|
||||
...item,
|
||||
status: "completed",
|
||||
executionState: "completed",
|
||||
} satisfies MessageStreamItem);
|
||||
}
|
||||
}
|
||||
return { items: changed ? nextItems : items, changed };
|
||||
}
|
||||
|
||||
function updateActiveSegment(
|
||||
state: ChatMessageStreamState,
|
||||
turnId: string,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ThreadGoal, ThreadGoalStatus } from "../../../../../domain/threads/goal";
|
||||
import { truncate } from "../../../../../utils";
|
||||
import { truncate } from "../../../../../shared/text/preview";
|
||||
import type { GoalMessageStreamItem } from "../items";
|
||||
|
||||
const GOAL_SUMMARY_LIMIT = 140;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { jsonPreview } from "../../../../../utils";
|
||||
import { jsonPreview } from "../../../../../shared/text/preview";
|
||||
|
||||
interface DetailRow {
|
||||
key: string;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ export interface PlanImplementationTarget {
|
|||
itemId: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamItemsEmptySource {
|
||||
items: readonly MessageStreamItem[];
|
||||
stableItems?: readonly MessageStreamItem[] | undefined;
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
}
|
||||
|
||||
export function messageStreamItemsEmpty(source: MessageStreamItemsEmptySource): boolean {
|
||||
if (!source.stableItems && !source.activeItems) return source.items.length === 0;
|
||||
return messageStreamSegmentsEmpty(source.stableItems ?? [], source.activeItems ?? []);
|
||||
}
|
||||
|
||||
export function messageStreamSegmentsEmpty(stableItems: readonly MessageStreamItem[], activeItems: readonly MessageStreamItem[]): boolean {
|
||||
return stableItems.length === 0 && activeItems.length === 0;
|
||||
}
|
||||
|
||||
export function forkCandidatesFromItems(items: readonly MessageStreamItem[]): readonly ForkCandidate[] {
|
||||
const turnOutcomeItemsByTurn = new Map<string, ForkCandidate>();
|
||||
for (const { item, capabilities } of messageStreamSemanticClassifications(items)) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { truncate } from "../../../../../utils";
|
||||
import { truncate } from "../../../../../shared/text/preview";
|
||||
import { collabAgentStateExecutionState } from "../agent-state";
|
||||
import type {
|
||||
AgentRunSummary,
|
||||
|
|
|
|||
|
|
@ -29,16 +29,22 @@ function mergeChanges(previous: MessageStreamItem, next: MessageStreamItem): rea
|
|||
return nextChanges && nextChanges.length > 0 ? nextChanges : previousChanges;
|
||||
}
|
||||
|
||||
export function completeReasoningItems(items: readonly MessageStreamItem[], turnId: string): MessageStreamItem[] {
|
||||
return items.map((item) =>
|
||||
item.kind === "reasoning" && item.turnId === turnId
|
||||
? {
|
||||
...item,
|
||||
status: "completed",
|
||||
executionState: "completed",
|
||||
}
|
||||
: item,
|
||||
);
|
||||
export function completeReasoningItems(items: readonly MessageStreamItem[], turnId: string): readonly MessageStreamItem[] {
|
||||
let changed = false;
|
||||
const nextItems: MessageStreamItem[] = [];
|
||||
for (const item of items) {
|
||||
if (item.kind !== "reasoning" || item.turnId !== turnId) {
|
||||
nextItems.push(item);
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
nextItems.push({
|
||||
...item,
|
||||
status: "completed",
|
||||
executionState: "completed",
|
||||
} satisfies MessageStreamItem);
|
||||
}
|
||||
return changed ? nextItems : items;
|
||||
}
|
||||
|
||||
export function attachHookRunsToTurn(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
type PendingUserInput,
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
import type { MessageStreamItem, MessageStreamUserInputQuestionResult } from "../message-stream/items";
|
||||
import { definedProp } from "../../../../utils";
|
||||
import { definedProp } from "../../../../shared/object/defined-prop";
|
||||
|
||||
export function createApprovalResultItem(approval: PendingApproval, action: ApprovalAction): MessageStreamItem {
|
||||
const kind = approvalActionKind(action);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { ChatAction, ChatDisclosureBucket, ChatDisclosureUiState } from "../../application/state/root-reducer";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamViewBlocks, type MessageStreamViewBlock } from "../../presentation/message-stream/view-model";
|
||||
import { type ForkCandidate, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import { messageStreamSegmentsEmpty, type ForkCandidate, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import type { MessageStreamRollbackCandidate } from "../../application/state/message-stream";
|
||||
import type { MessageStreamContext } from "../../ui/message-stream/context";
|
||||
import type { ChatPanelMessageStreamShellState } from "../shell-state";
|
||||
|
|
@ -142,7 +141,7 @@ function messageStreamStateProjection(
|
|||
state.forkCandidates,
|
||||
state.implementPlanTarget,
|
||||
);
|
||||
const pendingRequests = messageStreamBlockItemsEmpty(state.stableItems, state.activeItems) ? null : pendingRequestBlockFromState(state);
|
||||
const pendingRequests = messageStreamSegmentsEmpty(state.stableItems, state.activeItems) ? null : pendingRequestBlockFromState(state);
|
||||
|
||||
return {
|
||||
activeThreadId: state.activeThreadId,
|
||||
|
|
@ -208,7 +207,3 @@ function pendingRequestBlockFromState(
|
|||
snapshot: pendingRequestBlockSnapshotFromState(pendingRequestBlockStateFromChatState(state)),
|
||||
};
|
||||
}
|
||||
|
||||
function messageStreamBlockItemsEmpty(stableItems: readonly MessageStreamItem[], activeItems: readonly MessageStreamItem[]): boolean {
|
||||
return stableItems.length === 0 && activeItems.length === 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MessageStreamSemanticClassification } from "../../domain/message-stream/semantics/types";
|
||||
import type { AgentRunSummary, MessageStreamItem, TaskProgressMessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamItemsEmpty } from "../../domain/message-stream/selectors";
|
||||
import { activeTurnLiveItems, messageStreamItemsWithoutActiveTaskProgress } from "../../domain/message-stream/semantics/active-turn";
|
||||
import { messageStreamLayoutBlocks, type MessageStreamItemAnnotations, type MessageStreamLayoutBlock } from "./layout";
|
||||
import { detailView, type DetailView } from "./detail-view";
|
||||
|
|
@ -132,7 +133,7 @@ export function messageStreamViewBlocks(input: MessageStreamPresentationBlockInp
|
|||
|
||||
function messageStreamPresentationBlocks(input: MessageStreamPresentationBlockInput): MessageStreamPresentationBlock[] {
|
||||
const headerBlocks = historyPresentationBlocks(input);
|
||||
if (messageStreamBlockItemsEmpty(input)) {
|
||||
if (messageStreamItemsEmpty(input)) {
|
||||
return [...headerBlocks, { kind: "empty", key: "empty" }];
|
||||
}
|
||||
|
||||
|
|
@ -180,11 +181,6 @@ function pendingRequestPresentationBlocks(input: MessageStreamPresentationBlockI
|
|||
];
|
||||
}
|
||||
|
||||
function messageStreamBlockItemsEmpty(input: MessageStreamPresentationBlockInput): boolean {
|
||||
if (!input.stableItems && !input.activeItems) return input.items.length === 0;
|
||||
return (input.stableItems?.length ?? 0) === 0 && (input.activeItems?.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
function layoutBlocksForInput(input: MessageStreamPresentationBlockInput): MessageStreamLayoutBlock[] {
|
||||
const { activeTurnId } = input;
|
||||
if (!activeTurnId || !input.stableItems || !input.activeItems) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
approvalActionKind,
|
||||
defaultPendingApprovalOptions,
|
||||
type ApprovalAction,
|
||||
type PendingApproval,
|
||||
type PendingMcpElicitation,
|
||||
|
|
@ -136,13 +137,16 @@ function pendingApprovalViewModel(approval: PendingApproval): PendingApprovalVie
|
|||
|
||||
function approvalActionOptions(approval: PendingApproval): ApprovalActionOption[] {
|
||||
const options = approval.actionOptions;
|
||||
if (!options || options.length === 0) return defaultApprovalActionOptions();
|
||||
return options.map((option) => ({
|
||||
return (options && options.length > 0 ? options : defaultPendingApprovalOptions()).map(approvalActionOptionViewModel);
|
||||
}
|
||||
|
||||
function approvalActionOptionViewModel(option: { id: string; label: string; action: ApprovalAction }): ApprovalActionOption {
|
||||
return {
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
action: option.action,
|
||||
className: approvalActionClassName(option.action),
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
function pendingUserInputViewModel(input: PendingUserInput): PendingUserInputViewModel {
|
||||
|
|
@ -164,15 +168,6 @@ function pendingUserInputViewModel(input: PendingUserInput): PendingUserInputVie
|
|||
};
|
||||
}
|
||||
|
||||
function defaultApprovalActionOptions(): ApprovalActionOption[] {
|
||||
return [
|
||||
{ id: "accept", label: "Allow", action: "accept", className: "mod-cta" },
|
||||
{ id: "accept-session", label: "Allow session", action: "accept-session", className: "" },
|
||||
{ id: "decline", label: "Deny", action: "decline", className: "mod-warning" },
|
||||
{ id: "cancel", label: "Cancel", action: "cancel", className: "" },
|
||||
];
|
||||
}
|
||||
|
||||
function approvalActionClassName(action: ApprovalAction): string {
|
||||
const kind = approvalActionKind(action);
|
||||
if (kind === "accept") return "mod-cta";
|
||||
|
|
|
|||
3
src/shared/id/thread-id.ts
Normal file
3
src/shared/id/thread-id.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function shortThreadId(threadId: string): string {
|
||||
return threadId.length <= 8 ? threadId : threadId.slice(0, 8);
|
||||
}
|
||||
6
src/shared/object/defined-prop.ts
Normal file
6
src/shared/object/defined-prop.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export function definedProp<Key extends string, Value>(
|
||||
key: Key,
|
||||
value: Value | null | undefined,
|
||||
): Record<Key, Value> | Record<string, never> {
|
||||
return value === null || value === undefined ? {} : ({ [key]: value } as Record<Key, Value>);
|
||||
}
|
||||
11
src/shared/text/preview.ts
Normal file
11
src/shared/text/preview.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export function truncate(value: string, maxLength: number): string {
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value;
|
||||
}
|
||||
|
||||
export function jsonPreview(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
25
src/utils.ts
25
src/utils.ts
|
|
@ -1,26 +1,7 @@
|
|||
export function truncate(value: string, maxLength: number): string {
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value;
|
||||
}
|
||||
|
||||
export function jsonPreview(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function definedProp<Key extends string, Value>(
|
||||
key: Key,
|
||||
value: Value | null | undefined,
|
||||
): Record<Key, Value> | Record<string, never> {
|
||||
return value === null || value === undefined ? {} : ({ [key]: value } as Record<Key, Value>);
|
||||
}
|
||||
export { definedProp } from "./shared/object/defined-prop";
|
||||
export { jsonPreview, truncate } from "./shared/text/preview";
|
||||
export { shortThreadId } from "./shared/id/thread-id";
|
||||
|
||||
export function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export function shortThreadId(threadId: string): string {
|
||||
return threadId.length <= 8 ? threadId : threadId.slice(0, 8);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { resetRuntimeIntentToConfig, setRuntimeIntentValue } from "../../src/fea
|
|||
import {
|
||||
pendingRuntimeSettingsPatch,
|
||||
serviceTierRequestForThreadStart,
|
||||
} from "../../src/features/chat/application/runtime/thread-settings-update";
|
||||
} from "../../src/features/chat/app-server/runtime/thread-settings-update";
|
||||
import { contextSummary, rateLimitSummary } from "../../src/features/chat/presentation/runtime/status";
|
||||
|
||||
describe("runtime settings", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue