mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Model chat thread goals
This commit is contained in:
parent
edf95e8244
commit
02fe06e21e
15 changed files with 118 additions and 33 deletions
|
|
@ -99,7 +99,6 @@ const generatedAppServerImportLegacyFiles = [
|
|||
"src/features/chat/chat-state-actions.ts",
|
||||
"src/features/chat/chat-state.ts",
|
||||
"src/features/chat/display/agent.ts",
|
||||
"src/features/chat/display/goal-messages.ts",
|
||||
"src/features/chat/display/hooks.ts",
|
||||
"src/features/chat/display/labels.ts",
|
||||
"src/features/chat/display/permission-details.ts",
|
||||
|
|
@ -116,9 +115,6 @@ const generatedAppServerImportLegacyFiles = [
|
|||
"src/features/chat/threads/thread-history-controller.ts",
|
||||
"src/features/chat/threads/thread-rename-controller.ts",
|
||||
"src/features/chat/threads/thread-resume.ts",
|
||||
"src/features/chat/turns/slash-command-actions.ts",
|
||||
"src/features/chat/turns/slash-command-execution.ts",
|
||||
"src/features/chat/ui/goal-banner.tsx",
|
||||
"src/features/chat/ui/pending-request-message.tsx",
|
||||
];
|
||||
const codexPanelEslintPlugin = {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import type { ThreadForkResponse } from "../generated/app-server/v2/ThreadForkRe
|
|||
import type { ThreadGoalClearResponse } from "../generated/app-server/v2/ThreadGoalClearResponse";
|
||||
import type { ThreadGoalGetResponse } from "../generated/app-server/v2/ThreadGoalGetResponse";
|
||||
import type { ThreadGoalSetResponse } from "../generated/app-server/v2/ThreadGoalSetResponse";
|
||||
import type { ThreadGoalStatus } from "../generated/app-server/v2/ThreadGoalStatus";
|
||||
import type { ThreadInjectItemsResponse } from "../generated/app-server/v2/ThreadInjectItemsResponse";
|
||||
import type { ThreadListResponse } from "../generated/app-server/v2/ThreadListResponse";
|
||||
import type { ThreadReadResponse } from "../generated/app-server/v2/ThreadReadResponse";
|
||||
|
|
@ -45,6 +44,7 @@ import type { ServerNotification } from "../generated/app-server/ServerNotificat
|
|||
import type { ServerRequest } from "../generated/app-server/ServerRequest";
|
||||
import type { JsonValue } from "../generated/app-server/serde_json/JsonValue";
|
||||
import { toAppServerUserInput, type CodexInput } from "./request-input";
|
||||
import { appServerThreadGoalUpdate, type ThreadGoalUpdate } from "./thread-goal";
|
||||
import type { ServiceTierRequest, ThreadSettingsUpdate } from "./thread-settings";
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
||||
|
|
@ -293,11 +293,8 @@ export class AppServerClient {
|
|||
return this.request("thread/goal/get", { threadId });
|
||||
}
|
||||
|
||||
setThreadGoal(
|
||||
threadId: string,
|
||||
params: { objective?: string | null; status?: ThreadGoalStatus | null; tokenBudget?: number | null },
|
||||
): Promise<ThreadGoalSetResponse> {
|
||||
return this.request("thread/goal/set", { threadId, ...params });
|
||||
setThreadGoal(threadId: string, params: ThreadGoalUpdate): Promise<ThreadGoalSetResponse> {
|
||||
return this.request("thread/goal/set", { threadId, ...appServerThreadGoalUpdate(params) });
|
||||
}
|
||||
|
||||
clearThreadGoal(threadId: string): Promise<ThreadGoalClearResponse> {
|
||||
|
|
|
|||
59
src/app-server/thread-goal.ts
Normal file
59
src/app-server/thread-goal.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { ThreadGoal as AppServerThreadGoal } from "../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus as AppServerThreadGoalStatus } from "../generated/app-server/v2/ThreadGoalStatus";
|
||||
|
||||
export type ThreadGoalStatus = "active" | "paused" | "blocked" | "usageLimited" | "budgetLimited" | "complete";
|
||||
|
||||
export interface ThreadGoal {
|
||||
threadId: string;
|
||||
objective: string;
|
||||
status: ThreadGoalStatus;
|
||||
tokenBudget: number | null;
|
||||
tokensUsed: number;
|
||||
timeUsedSeconds: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface ThreadGoalUpdate {
|
||||
objective?: string | null;
|
||||
status?: ThreadGoalStatus | null;
|
||||
tokenBudget?: number | null;
|
||||
}
|
||||
|
||||
export function threadGoalFromAppServerGoal(goal: AppServerThreadGoal | null): ThreadGoal | null {
|
||||
if (!goal) return null;
|
||||
return {
|
||||
threadId: goal.threadId,
|
||||
objective: goal.objective,
|
||||
status: threadGoalStatusFromAppServerStatus(goal.status),
|
||||
tokenBudget: goal.tokenBudget,
|
||||
tokensUsed: finiteNumber(goal.tokensUsed),
|
||||
timeUsedSeconds: finiteNumber(goal.timeUsedSeconds),
|
||||
createdAt: finiteNumber(goal.createdAt),
|
||||
updatedAt: finiteNumber(goal.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
export function appServerThreadGoalUpdate(update: ThreadGoalUpdate): {
|
||||
objective?: string | null;
|
||||
status?: AppServerThreadGoalStatus | null;
|
||||
tokenBudget?: number | null;
|
||||
} {
|
||||
return {
|
||||
...("objective" in update ? { objective: update.objective } : {}),
|
||||
...("status" in update ? { status: update.status === null ? null : appServerThreadGoalStatus(update.status) } : {}),
|
||||
...("tokenBudget" in update ? { tokenBudget: update.tokenBudget } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function threadGoalStatusFromAppServerStatus(status: AppServerThreadGoalStatus): ThreadGoalStatus {
|
||||
return status;
|
||||
}
|
||||
|
||||
function appServerThreadGoalStatus(status: ThreadGoalStatus): AppServerThreadGoalStatus {
|
||||
return status;
|
||||
}
|
||||
|
||||
function finiteNumber(value: number): number {
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import type { InitializeResponse } from "../../generated/app-server/InitializeRe
|
|||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import type { ModelMetadata, SkillMetadata } from "../../domain/catalog/metadata";
|
||||
import type { ThreadGoal } from "../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoal } from "../../app-server/thread-goal";
|
||||
import type { ThreadSettingsUpdate } from "../../app-server/thread-settings";
|
||||
import type { ApprovalsReviewer } from "../../app-server/runtime-policy";
|
||||
import type { Diagnostics } from "../../app-server/diagnostics";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { ThreadGoal } from "../../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus } from "../../../generated/app-server/v2/ThreadGoalStatus";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../app-server/thread-goal";
|
||||
import { truncate } from "../../../utils";
|
||||
import type { GoalDisplayItem } from "./types";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import type { AppServerClient } from "../../../app-server/client";
|
||||
import {
|
||||
threadGoalFromAppServerGoal,
|
||||
type ThreadGoal,
|
||||
type ThreadGoalStatus,
|
||||
type ThreadGoalUpdate,
|
||||
} from "../../../app-server/thread-goal";
|
||||
import type { JsonValue } from "../../../generated/app-server/serde_json/JsonValue";
|
||||
import type { ThreadGoal } from "../../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus } from "../../../generated/app-server/v2/ThreadGoalStatus";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
import type { GoalDisplayItem } from "../display/types";
|
||||
import { goalChangeItem } from "../display/goal-messages";
|
||||
|
|
@ -39,7 +43,7 @@ async function syncThreadGoal(host: ChatThreadGoalActionsHost, threadId: string)
|
|||
if (!client) return;
|
||||
try {
|
||||
const response = await client.getThreadGoal(threadId);
|
||||
applyGoalIfActive(host, threadId, response.goal, { reportChange: false });
|
||||
applyGoalIfActive(host, threadId, threadGoalFromAppServerGoal(response.goal), { reportChange: false });
|
||||
} catch (error) {
|
||||
if (host.stateStore.getState().activeThread.id !== threadId) return;
|
||||
host.addSystemMessage(`Could not load thread goal: ${errorMessage(error)}`);
|
||||
|
|
@ -88,17 +92,13 @@ async function clearGoal(host: ChatThreadGoalActionsHost, threadId: string): Pro
|
|||
}
|
||||
}
|
||||
|
||||
async function setGoal(
|
||||
host: ChatThreadGoalActionsHost,
|
||||
threadId: string,
|
||||
params: { objective?: string | null; status?: ThreadGoalStatus | null; tokenBudget?: number | null },
|
||||
): Promise<boolean> {
|
||||
async function setGoal(host: ChatThreadGoalActionsHost, threadId: string, params: ThreadGoalUpdate): Promise<boolean> {
|
||||
await host.ensureConnected();
|
||||
const client = host.currentClient();
|
||||
if (!client) return false;
|
||||
try {
|
||||
const response = await client.setThreadGoal(threadId, params);
|
||||
return applyGoalIfActive(host, threadId, response.goal, { reportChange: true });
|
||||
return applyGoalIfActive(host, threadId, threadGoalFromAppServerGoal(response.goal), { reportChange: true });
|
||||
} catch (error) {
|
||||
host.addSystemMessage(errorMessage(error));
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import {
|
|||
import type { SlashCommandName } from "../composer/slash-commands";
|
||||
import type { DisplayDetailSection } from "../display/types";
|
||||
import type { ReasoningEffort } from "../../../domain/catalog/metadata";
|
||||
import type { ThreadGoal } from "../../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus } from "../../../generated/app-server/v2/ThreadGoalStatus";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../app-server/thread-goal";
|
||||
import { submissionStateSnapshot } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import type { CodexInput } from "../../../app-server/request-input";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../app-server/thread-goal";
|
||||
import type { ReasoningEffort } from "../../../domain/catalog/metadata";
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import type { ThreadGoal } from "../../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus } from "../../../generated/app-server/v2/ThreadGoalStatus";
|
||||
import { getThreadTitle } from "../../../domain/threads/model";
|
||||
import type { ReferencedThreadDisplay } from "../../../domain/threads/reference";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { ThreadGoal } from "../../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus } from "../../../generated/app-server/v2/ThreadGoalStatus";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../app-server/thread-goal";
|
||||
import { isComposerSendKey, type SendShortcut } from "../../../shared/ui/keyboard";
|
||||
import { IconButton } from "../../../shared/ui/components";
|
||||
import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow";
|
||||
|
|
|
|||
37
tests/app-server/thread-goal.test.ts
Normal file
37
tests/app-server/thread-goal.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { appServerThreadGoalUpdate, threadGoalFromAppServerGoal } from "../../src/app-server/thread-goal";
|
||||
|
||||
describe("app-server thread goal model", () => {
|
||||
it("maps app-server goals into panel-owned goal snapshots", () => {
|
||||
expect(
|
||||
threadGoalFromAppServerGoal({
|
||||
threadId: "thread",
|
||||
objective: "Finish",
|
||||
status: "active",
|
||||
tokenBudget: 100,
|
||||
tokensUsed: Number.POSITIVE_INFINITY,
|
||||
timeUsedSeconds: 12,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
}),
|
||||
).toEqual({
|
||||
threadId: "thread",
|
||||
objective: "Finish",
|
||||
status: "active",
|
||||
tokenBudget: 100,
|
||||
tokensUsed: 0,
|
||||
timeUsedSeconds: 12,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("serializes partial goal updates without inventing omitted fields", () => {
|
||||
expect(appServerThreadGoalUpdate({ status: "paused" })).toEqual({ status: "paused" });
|
||||
expect(appServerThreadGoalUpdate({ objective: "Updated", tokenBudget: null })).toEqual({
|
||||
objective: "Updated",
|
||||
tokenBudget: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,9 +10,9 @@ import {
|
|||
transitionChatTurnLifecycleState,
|
||||
type ChatState,
|
||||
} from "../../../src/features/chat/chat-state";
|
||||
import type { ThreadGoal } from "../../../src/app-server/thread-goal";
|
||||
import type { DisplayItem } from "../../../src/features/chat/display/types";
|
||||
import type { Thread } from "../../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadGoal } from "../../../src/generated/app-server/v2/ThreadGoal";
|
||||
|
||||
describe("chatReducer", () => {
|
||||
it("clears active turn and thread-scoped state", () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ThreadGoal } from "../../../../src/app-server/thread-goal";
|
||||
import { goalChangeItem } from "../../../../src/features/chat/display/goal-messages";
|
||||
import type { ThreadGoal } from "../../../../src/generated/app-server/v2/ThreadGoal";
|
||||
|
||||
describe("goal display items", () => {
|
||||
it("keeps goal event summaries compact while retaining the full objective in details", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient } from "../../../../src/app-server/client";
|
||||
import type { ThreadGoal } from "../../../../src/app-server/thread-goal";
|
||||
import { createChatThreadGoalActions } from "../../../../src/features/chat/threads/thread-goal-actions";
|
||||
import { createChatState, createChatStateStore } from "../../../../src/features/chat/chat-state";
|
||||
import type { ThreadGoal } from "../../../../src/generated/app-server/v2/ThreadGoal";
|
||||
|
||||
describe("createChatThreadGoalActions", () => {
|
||||
it("syncs the active thread goal into chat state", async () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ThreadGoal } from "../../../../src/app-server/thread-goal";
|
||||
import { slashCommandHelpLines, slashCommandHelpSections } from "../../../../src/features/chat/composer/slash-commands";
|
||||
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadGoal } from "../../../../src/generated/app-server/v2/ThreadGoal";
|
||||
import { executeSlashCommand, type SlashCommandExecutionContext } from "../../../../src/features/chat/turns/slash-command-execution";
|
||||
|
||||
function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCommandExecutionContext {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { act } from "preact/test-utils";
|
||||
|
||||
import type { ThreadGoal } from "../../../../../src/app-server/thread-goal";
|
||||
import { renderGoalBanner } from "../../../../../src/features/chat/ui/goal-banner";
|
||||
import type { ThreadGoal } from "../../../../../src/generated/app-server/v2/ThreadGoal";
|
||||
import type { SendShortcut } from "../../../../../src/shared/ui/keyboard";
|
||||
import { installObsidianDomShims } from "../../../../support/dom";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue