diff --git a/docs/architecture/deepchat-tape-baseline/plan.md b/docs/architecture/deepchat-tape-baseline/plan.md new file mode 100644 index 000000000..9e5536c62 --- /dev/null +++ b/docs/architecture/deepchat-tape-baseline/plan.md @@ -0,0 +1,12 @@ +# DeepChat Tape Baseline Plan + +## Approach + +Keep the Tape implementation baseline as a goal-scoped architecture document. +Use it as the shared map for the active Tape SDD folders. + +## Maintenance + +- Keep references to active Tape SDD folders relative to this directory. +- Update the baseline when ownership or runtime flow changes. +- Keep compatibility notes aligned with the current Tape schema. diff --git a/docs/architecture/deepchat_tape_spec_v1.md b/docs/architecture/deepchat-tape-baseline/spec.md similarity index 93% rename from docs/architecture/deepchat_tape_spec_v1.md rename to docs/architecture/deepchat-tape-baseline/spec.md index 2613b17a7..5bcbc9778 100644 --- a/docs/architecture/deepchat_tape_spec_v1.md +++ b/docs/architecture/deepchat-tape-baseline/spec.md @@ -1,12 +1,12 @@ # DeepChat Tape System - Implementation Baseline Status: current implementation direction. Active SDD goals are -[deepchat-tape-view-manifest](deepchat-tape-view-manifest/spec.md), -[deepchat-tape-replay-contract](deepchat-tape-replay-contract/spec.md), and -[deepchat-tape-view-assembler](deepchat-tape-view-assembler/spec.md), and -[deepchat-tape-view-policy](deepchat-tape-view-policy/spec.md), and -[deepchat-tape-policy-provenance](deepchat-tape-policy-provenance/spec.md), and -[deepchat-tape-policy-selector](deepchat-tape-policy-selector/spec.md). +[deepchat-tape-view-manifest](../deepchat-tape-view-manifest/spec.md), +[deepchat-tape-replay-contract](../deepchat-tape-replay-contract/spec.md), and +[deepchat-tape-view-assembler](../deepchat-tape-view-assembler/spec.md), and +[deepchat-tape-view-policy](../deepchat-tape-view-policy/spec.md), and +[deepchat-tape-policy-provenance](../deepchat-tape-policy-provenance/spec.md), and +[deepchat-tape-policy-selector](../deepchat-tape-policy-selector/spec.md). This document keeps the Tape vision aligned with the current DeepChat codebase. The implementation path is: diff --git a/docs/architecture/deepchat-tape-baseline/tasks.md b/docs/architecture/deepchat-tape-baseline/tasks.md new file mode 100644 index 000000000..36156603f --- /dev/null +++ b/docs/architecture/deepchat-tape-baseline/tasks.md @@ -0,0 +1,5 @@ +# DeepChat Tape Baseline Tasks + +- [x] Move the baseline spec into a kebab-case architecture folder. +- [x] Update relative links to active Tape SDD folders. +- [x] Update references from the legacy flat architecture path. diff --git a/docs/architecture/deepchat-tape-policy-selector/plan.md b/docs/architecture/deepchat-tape-policy-selector/plan.md index 0097805c8..72e2fbf47 100644 --- a/docs/architecture/deepchat-tape-policy-selector/plan.md +++ b/docs/architecture/deepchat-tape-policy-selector/plan.md @@ -27,7 +27,7 @@ TapeViewAssembler.buildTapeResumeView() | `src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts` | Resolve default policy through selector. | | `test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts` | Cover registry and selector behavior. | | `test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts` | Assert selection reason and injected policy behavior. | -| `docs/architecture/deepchat_tape_spec_v1.md` | Record the selector boundary. | +| `docs/architecture/deepchat-tape-baseline/spec.md` | Record the selector boundary. | ## Compatibility diff --git a/docs/architecture/deepchat-tape-view-assembler/plan.md b/docs/architecture/deepchat-tape-view-assembler/plan.md index ced829cd6..3626226fe 100644 --- a/docs/architecture/deepchat-tape-view-assembler/plan.md +++ b/docs/architecture/deepchat-tape-view-assembler/plan.md @@ -36,7 +36,7 @@ resumeAssistantMessage() | `src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts` | Selection policy boundary used by the assembler. | | `src/main/presenter/agentRuntimePresenter/index.ts` | Replace direct metadata builder calls with assembler calls. | | `test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts` | Add chat/resume parity tests. | -| `docs/architecture/deepchat_tape_spec_v1.md` | Update current implementation path and owner table. | +| `docs/architecture/deepchat-tape-baseline/spec.md` | Update current implementation path and owner table. | ## Compatibility diff --git a/docs/architecture/deepchat-tape-view-policy/plan.md b/docs/architecture/deepchat-tape-view-policy/plan.md index c81fcfcb4..a73365039 100644 --- a/docs/architecture/deepchat-tape-view-policy/plan.md +++ b/docs/architecture/deepchat-tape-view-policy/plan.md @@ -27,7 +27,7 @@ TapeViewAssembler.buildTapeResumeView() | `src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts` | Delegate selection to `TapeViewPolicy`. | | `test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts` | Prove legacy policy parity. | | `test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts` | Assert policy metadata and policy delegation. | -| `docs/architecture/deepchat_tape_spec_v1.md` | Record the new policy boundary. | +| `docs/architecture/deepchat-tape-baseline/spec.md` | Record the new policy boundary. | ## Compatibility diff --git a/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md b/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md index 657255637..f574c45ca 100644 --- a/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md +++ b/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md @@ -15,6 +15,9 @@ Apply the PR review fixes in place, keeping the existing Tape architecture and p | `TraceDialog.vue` | When a request sequence is selected, only show matching trace/manifest data. | | `routes.ts` | Replace broad route catalog annotation with `satisfies Record`. | | i18n `traceDialog.json` | Translate new diagnostic keys for non-English locales and convert Traditional Chinese files. | +| `contextBuilder.ts` | Preserve turn boundaries during emergency truncation. | +| `tapeService.ts` | Exclude replay export timestamps from slice hash inputs. | +| `tapeViewManifest.ts` | Copy included/excluded ref arrays into manifest snapshots. | | SDD docs | Keep this issue SDD current and address small doc nitpicks. | ## Compatibility @@ -40,5 +43,7 @@ pnpm run typecheck pnpm vitest run test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeService.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts pnpm vitest run test/renderer/components/trace/TraceDialog.test.ts ``` diff --git a/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md b/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md index 3ab6b0799..08ac1e042 100644 --- a/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md +++ b/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md @@ -1,18 +1,18 @@ # Tape ViewManifest PR Review Fixes - Spec -Status: active issue-fix SDD for PR #1767 review follow-up. +Status: active issue-fix SDD for PR #1768 review follow-up. ## Problem -PR review identified correctness and localization issues in the Tape ViewManifest flow. Some diagnostics can fail hard instead of degrading gracefully, request/manifest diagnostics can show mismatched request sequences, manifest provenance can record stale summary cursors after context-pressure recovery, resume view assembly can use stale tape history after compaction, route-contract typing loses literal key precision, and newly added TraceDialog labels are not properly localized. +PR review identified correctness and localization issues in the Tape ViewManifest flow. Some diagnostics can fail hard instead of degrading gracefully, request/manifest diagnostics can show mismatched request sequences, manifest provenance can record stale summary cursors after context-pressure recovery, resume view assembly can use stale tape history after compaction, request sequence generation can repeat after resume, replay hashes can include wall-clock time, manifest snapshots can alias caller arrays, and newly added TraceDialog labels are not properly localized. ## Goals -1. Fix still-valid CodeRabbit review findings for PR #1767 with minimal changes. +1. Fix still-valid CodeRabbit review findings for PR #1768 with minimal changes. 2. Preserve Tape ViewManifest and replay-slice contracts while correcting provenance and diagnostics behavior. 3. Ensure TraceDialog diagnostic strings are localized for every supported non-English locale touched by the PR. 4. Keep route-contract literal key inference intact. -5. Commit the fixes locally without pushing. +5. Commit and push the fixes to the existing PR branch. ## Acceptance Criteria @@ -25,11 +25,13 @@ PR review identified correctness and localization issues in the Tape ViewManifes 7. `DEEPCHAT_ROUTE_CATALOG` uses `satisfies Record` so route names remain a literal union. 8. TraceDialog diagnostic labels are translated in supported non-English locale files, including Traditional Chinese variants. 9. Review nitpicks that are small and local are addressed without broad refactors. -10. `pnpm run format`, `pnpm run i18n`, and `pnpm run lint` pass or any blocker is documented. +10. Emergency history truncation preserves per-turn metadata associations. +11. Replay slice hashes are deterministic across exports of the same manifest. +12. Manifest included/excluded snapshots are detached from caller-owned arrays. +13. `pnpm run format`, `pnpm run i18n`, and `pnpm run lint` pass or any blocker is documented. ## Constraints -- Do not push changes. - Do not weaken authentication, authorization, or validation. - Avoid unrelated refactors and preserve existing presenter boundaries. - Keep ViewManifest schema compatible. diff --git a/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md b/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md index 9fc46b6a6..b09af22c0 100644 --- a/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md +++ b/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md @@ -8,3 +8,9 @@ - [x] T6: Address local documentation/nitpick comments. - [x] T7: Run format, i18n, lint, and focused validation. - [x] T8: Stage intentional files and create a local commit without pushing. +- [x] T9: Preserve turn metadata during emergency context truncation. +- [x] T10: Make replay slice hashes deterministic across export times. +- [x] T11: Detach manifest included/excluded snapshots from caller-owned arrays. +- [x] T12: Localize TraceDialog fallback values and Traditional Chinese model labels. +- [x] T13: Restore MCP checkbox test semantics and PascalCase component filename. +- [x] T14: Run required checks, commit, and push the PR branch. diff --git a/docs/issues/mcp-server-form-auto-approve-controls/plan.md b/docs/issues/mcp-server-form-auto-approve-controls/plan.md new file mode 100644 index 000000000..9702e007b --- /dev/null +++ b/docs/issues/mcp-server-form-auto-approve-controls/plan.md @@ -0,0 +1,15 @@ +# MCP Server Form Auto Approve Controls Plan + +## Approach + +Restore the existing checkbox component binding for the MCP server form auto-approve options. +Keep the submitted `MCPServerConfig.autoApprove` shape unchanged. + +## Implementation + +- Import the shared checkbox component used by the form template. +- Verify edit-mode initial values and submit behavior for read/write permissions. + +## Verification + +- `pnpm vitest --config vitest.config.renderer.ts test/renderer/components/McpServerForm.test.ts` diff --git a/docs/issues/mcp-server-form-auto-approve-controls/spec.md b/docs/issues/mcp-server-form-auto-approve-controls/spec.md new file mode 100644 index 000000000..6b19b10ac --- /dev/null +++ b/docs/issues/mcp-server-form-auto-approve-controls/spec.md @@ -0,0 +1,36 @@ +# MCP Server Form Auto Approve Controls Spec + +## Goal + +Restore editable auto-approve controls in the MCP server add/edit form. + +## Requirements + +- The MCP add server form displays interactive controls for All, Read, and Write auto-approve options. +- The MCP edit server form displays the same controls and initializes them from `initialConfig.autoApprove`. +- Submitting the form persists the selected values through `MCPServerConfig.autoApprove`. +- Existing server fields, route contracts, and store behavior remain unchanged. + +## Layout + +Before: + +```text +Auto Approve + All + Read + Write +``` + +After: + +```text +Auto Approve + [ ] All + [ ] Read + [ ] Write +``` + +## Compatibility + +MCP config keys and saved `autoApprove` values remain unchanged. diff --git a/docs/issues/mcp-server-form-auto-approve-controls/tasks.md b/docs/issues/mcp-server-form-auto-approve-controls/tasks.md new file mode 100644 index 000000000..37a2f2a59 --- /dev/null +++ b/docs/issues/mcp-server-form-auto-approve-controls/tasks.md @@ -0,0 +1,6 @@ +# MCP Server Form Auto Approve Controls Tasks + +- [x] Restore the checkbox component import. +- [x] Add a renderer component test for editable auto-approve controls. +- [x] Assert selected read/write permissions are submitted through `autoApprove`. +- [x] Run targeted renderer test. diff --git a/docs/issues/settings-save-clone-errors/plan.md b/docs/issues/settings-save-clone-errors/plan.md new file mode 100644 index 000000000..0abcc4159 --- /dev/null +++ b/docs/issues/settings-save-clone-errors/plan.md @@ -0,0 +1,17 @@ +# Settings Save Clone Errors Plan + +## Approach + +Normalize renderer-owned settings payloads into plain objects before invoking config routes. +Keep route names, persisted config keys, and presenter contracts unchanged. + +## Implementation + +- Add a small recursive serializer in the renderer config client for arrays, objects, and dates. +- Apply the serializer to settings save paths that can receive Vue reactive proxies. +- Normalize DeepChat Agent model selections before building create/update payloads. +- Cover serialized bridge payloads with structured clone assertions. + +## Verification + +- `pnpm vitest --config vitest.config.renderer.ts test/renderer/api/clients.test.ts test/renderer/components/DeepChatAgentsSettings.test.ts` diff --git a/docs/issues/settings-save-clone-errors/spec.md b/docs/issues/settings-save-clone-errors/spec.md new file mode 100644 index 000000000..0e3cb459c --- /dev/null +++ b/docs/issues/settings-save-clone-errors/spec.md @@ -0,0 +1,19 @@ +# Settings Save Clone Errors Spec + +## Goal + +Fix renderer IPC clone errors when settings save paths receive reactive objects. + +## Requirements + +- Saving an existing DeepChat Agent sends a structured-cloneable payload to the typed route bridge. +- Creating a DeepChat Agent uses the same structured-cloneable payload shape as updating. +- Adding, updating, and replacing custom prompts send structured-cloneable payloads. +- Adding, updating, and replacing system prompts send structured-cloneable payloads. +- Saving shortcut keys sends a structured-cloneable payload. +- Existing saved values and route contracts remain unchanged. +- Tests cover the renderer API client payloads with structured clone validation. + +## Compatibility + +Settings route names, presenter contracts, and persisted config keys remain unchanged. diff --git a/docs/issues/settings-save-clone-errors/tasks.md b/docs/issues/settings-save-clone-errors/tasks.md new file mode 100644 index 000000000..91d2319e5 --- /dev/null +++ b/docs/issues/settings-save-clone-errors/tasks.md @@ -0,0 +1,8 @@ +# Settings Save Clone Errors Tasks + +- [x] Identify settings save routes that can receive reactive objects. +- [x] Serialize shortcut key, custom prompt, system prompt, and DeepChat Agent payloads. +- [x] Normalize DeepChat Agent advanced model selections to plain route values. +- [x] Add renderer API client structured clone coverage. +- [x] Add DeepChat Agent settings save coverage for cloneable model selections. +- [x] Run targeted renderer tests. diff --git a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts index b88edd218..3ebd2b2e3 100644 --- a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts +++ b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts @@ -923,13 +923,34 @@ function selectTurnHistoryTurns( } const truncatedMessages = truncateContext(flattened, availableTokens) - return [ - { - ...remainingTurns[0], - messages: truncatedMessages, - tokens: estimateMessagesTokens(truncatedMessages) + if (truncatedMessages.length === 0) { + return [] + } + + let droppedPrefixCount = flattened.length - truncatedMessages.length + const rebuiltTurns: T[] = [] + + for (const turn of remainingTurns) { + if (droppedPrefixCount >= turn.messages.length) { + droppedPrefixCount -= turn.messages.length + continue } - ] + + if (droppedPrefixCount > 0) { + const keptMessages = turn.messages.slice(droppedPrefixCount) + droppedPrefixCount = 0 + rebuiltTurns.push({ + ...turn, + messages: keptMessages, + tokens: estimateMessagesTokens(keptMessages) + }) + continue + } + + rebuiltTurns.push(turn) + } + + return rebuiltTurns } function filterRecordsFromCursor( diff --git a/src/main/presenter/agentRuntimePresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts index a46fd9a9b..57d59cb0c 100644 --- a/src/main/presenter/agentRuntimePresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -2419,7 +2419,8 @@ export class AgentRuntimePresenter implements IAgentImplementation { const rateLimitMessageId = this.buildRateLimitStreamMessageId(activeGeneration.runId) const emitRateLimitWaitingMessage = this.emitRateLimitWaitingMessage.bind(this) const clearRateLimitWaitingMessage = this.clearRateLimitWaitingMessage.bind(this) - let requestSeq = 0 + let requestSeq = + this.tapeService.listViewManifestsByMessage(sessionId, messageId)[0]?.requestSeq ?? 0 try { this.dispatchHook('SessionStart', { diff --git a/src/main/presenter/agentRuntimePresenter/tapeService.ts b/src/main/presenter/agentRuntimePresenter/tapeService.ts index f051ce4d5..a91832089 100644 --- a/src/main/presenter/agentRuntimePresenter/tapeService.ts +++ b/src/main/presenter/agentRuntimePresenter/tapeService.ts @@ -346,11 +346,13 @@ function withReplaySliceHash( hashes: Omit & { sliceHash: '' } } ): DeepChatTapeReplaySlice { + const sliceForHash = { ...slice } as Partial + delete sliceForHash.createdAt return { ...slice, hashes: { ...slice.hashes, - sliceHash: hashJson(slice) + sliceHash: hashJson(sliceForHash) } } } diff --git a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts index 9e798363c..017d887f8 100644 --- a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts +++ b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts @@ -171,8 +171,8 @@ export function createTapeViewManifest( contextBuilderVersion: TAPE_VIEW_CONTEXT_BUILDER_VERSION, latestEntryId: input.latestEntryId, anchorEntryIds: [...input.anchorEntryIds], - included: input.included, - excluded: input.excluded, + included: input.included.map((entry) => ({ ...entry })), + excluded: input.excluded.map((entry) => ({ ...entry })), tokenBudget: { ...input.tokenBudget, estimatedPromptTokens: estimateMessagesTokens(input.messages) diff --git a/src/renderer/api/ConfigClient.ts b/src/renderer/api/ConfigClient.ts index 278ea154c..293a6445e 100644 --- a/src/renderer/api/ConfigClient.ts +++ b/src/renderer/api/ConfigClient.ts @@ -161,6 +161,27 @@ function toPlainKnowledgeConfigs(configs: BuiltinKnowledgeConfig[]): BuiltinKnow }) } +function toPlainIpcValue(value: T): T { + if (value === null || typeof value !== 'object') { + return value + } + + if (value instanceof Date) { + return new Date(value.getTime()) as T + } + + if (Array.isArray(value)) { + return value.map((item) => toPlainIpcValue(item)) as T + } + + const plain: Record = {} + for (const [key, nestedValue] of Object.entries(value as Record)) { + plain[key] = toPlainIpcValue(nestedValue) + } + + return plain as T +} + export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) { const settingsClient = createSettingsClient(bridge) @@ -321,7 +342,9 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) } async function setShortcutKey(shortcuts: ShortcutKeySetting) { - return await bridge.invoke(configSetShortcutKeysRoute.name, { shortcuts }) + return await bridge.invoke(configSetShortcutKeysRoute.name, { + shortcuts: toPlainIpcValue(shortcuts) + }) } async function resetShortcutKeys() { @@ -335,20 +358,20 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) async function setCustomPrompts(prompts: Prompt[]) { return await bridge.invoke(configSetCustomPromptsRoute.name, { - prompts: prompts as any + prompts: toPlainIpcValue(prompts) as any }) } async function addCustomPrompt(prompt: Prompt) { return await bridge.invoke(configAddCustomPromptRoute.name, { - prompt: prompt as any + prompt: toPlainIpcValue(prompt) as any }) } async function updateCustomPrompt(promptId: string, updates: Partial) { return await bridge.invoke(configUpdateCustomPromptRoute.name, { promptId, - updates: updates as any + updates: toPlainIpcValue(updates) as any }) } @@ -385,20 +408,20 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) async function setSystemPrompts(prompts: SystemPrompt[]) { return await bridge.invoke(configSetSystemPromptsRoute.name, { - prompts: prompts as any + prompts: toPlainIpcValue(prompts) as any }) } async function addSystemPrompt(prompt: SystemPrompt) { return await bridge.invoke(configAddSystemPromptRoute.name, { - prompt: prompt as any + prompt: toPlainIpcValue(prompt) as any }) } async function updateSystemPrompt(promptId: string, updates: Partial) { return await bridge.invoke(configUpdateSystemPromptRoute.name, { promptId, - updates: updates as any + updates: toPlainIpcValue(updates) as any }) } @@ -495,7 +518,7 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) async function createDeepChatAgent(input: CreateDeepChatAgentInput): Promise { const result = await bridge.invoke( configCreateDeepChatAgentRoute.name, - input as DeepchatRouteInput + toPlainIpcValue(input) as DeepchatRouteInput ) return result.agent } @@ -506,7 +529,7 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) ): Promise { const result = await bridge.invoke(configUpdateDeepChatAgentRoute.name, { agentId, - updates + updates: toPlainIpcValue(updates) } as DeepchatRouteInput) return result.agent } diff --git a/src/renderer/settings/components/DeepChatAgentsSettings.vue b/src/renderer/settings/components/DeepChatAgentsSettings.vue index ea52a567f..652c886b1 100644 --- a/src/renderer/settings/components/DeepChatAgentsSettings.vue +++ b/src/renderer/settings/components/DeepChatAgentsSettings.vue @@ -673,6 +673,8 @@ import type { Agent, AgentAvatar as AgentAvatarValue, AgentTransferImpact, + CreateDeepChatAgentInput, + DeepChatAgentModelSelection, DeepChatSubagentSlot, PermissionMode, Project @@ -1061,6 +1063,15 @@ const normalizePath = (value: string | null | undefined) => { const normalized = value?.trim() return normalized ? normalized : null } +const buildModelSelection = ( + selection: EditableModel | null | undefined +): DeepChatAgentModelSelection | null => + selection + ? { + providerId: selection.providerId, + modelId: selection.modelId + } + : null const normalizeNumericInput = ( value: EditableNumberValue | null | undefined, options: { fallback: number; min: number; max: number; integer?: boolean } @@ -1358,21 +1369,16 @@ const saveAgent = async () => { if (!form.name.trim()) return saving.value = true try { - const payload = { + const payload: CreateDeepChatAgentInput = { name: form.name.trim(), enabled: form.enabled, description: form.description.trim() || undefined, avatar: buildAvatar(), config: { - defaultModelPreset: form.chatModel - ? { - providerId: form.chatModel.providerId, - modelId: form.chatModel.modelId - } - : null, - assistantModel: form.assistantModel, - visionModel: form.visionModel, - imageGenerationModel: form.imageGenerationModel, + defaultModelPreset: buildModelSelection(form.chatModel), + assistantModel: buildModelSelection(form.assistantModel), + visionModel: buildModelSelection(form.visionModel), + imageGenerationModel: buildModelSelection(form.imageGenerationModel), defaultProjectPath: normalizePath(form.defaultProjectPath), systemPrompt: form.systemPrompt, permissionMode: form.permissionMode, diff --git a/src/renderer/src/components/mcp-config/mcpServerForm.vue b/src/renderer/src/components/mcp-config/McpServerForm.vue similarity index 99% rename from src/renderer/src/components/mcp-config/mcpServerForm.vue rename to src/renderer/src/components/mcp-config/McpServerForm.vue index 5230924e6..cbe693d2d 100644 --- a/src/renderer/src/components/mcp-config/mcpServerForm.vue +++ b/src/renderer/src/components/mcp-config/McpServerForm.vue @@ -2,6 +2,7 @@ import { ref, computed, watch } from 'vue' import { useI18n } from 'vue-i18n' import { Button } from '@shadcn/components/ui/button' +import { Checkbox } from '@shadcn/components/ui/checkbox' import { Input } from '@shadcn/components/ui/input' import { Label } from '@shadcn/components/ui/label' import { Textarea } from '@shadcn/components/ui/textarea' diff --git a/src/renderer/src/components/mcp-config/components/McpServers.vue b/src/renderer/src/components/mcp-config/components/McpServers.vue index 7ea3a4e46..82fc565dc 100644 --- a/src/renderer/src/components/mcp-config/components/McpServers.vue +++ b/src/renderer/src/components/mcp-config/components/McpServers.vue @@ -25,7 +25,7 @@ import { useI18n } from 'vue-i18n' import { useToast } from '@/components/use-toast' import { useRouter } from 'vue-router' import McpServerCard from './McpServerCard.vue' -import McpServerForm from '../mcpServerForm.vue' +import McpServerForm from '../McpServerForm.vue' import McpToolPanel from './McpToolPanel.vue' import McpPromptPanel from './McpPromptPanel.vue' import McpResourceViewer from './McpResourceViewer.vue' diff --git a/src/renderer/src/components/mcp-config/index.ts b/src/renderer/src/components/mcp-config/index.ts index c2b647e13..d43bb1ab7 100644 --- a/src/renderer/src/components/mcp-config/index.ts +++ b/src/renderer/src/components/mcp-config/index.ts @@ -1,5 +1,5 @@ import McpConfig from './mcpConfig.vue' -import McpServerForm from './mcpServerForm.vue' +import McpServerForm from './McpServerForm.vue' export { McpConfig, McpServerForm } export default McpConfig diff --git a/src/renderer/src/components/trace/TraceDialog.vue b/src/renderer/src/components/trace/TraceDialog.vue index c9eb52ffb..9154d753e 100644 --- a/src/renderer/src/components/trace/TraceDialog.vue +++ b/src/renderer/src/components/trace/TraceDialog.vue @@ -39,11 +39,15 @@
{{ t('traceDialog.provider') }}: - {{ diagnosticProviderId || '-' }} + {{ + diagnosticProviderId || t('traceDialog.notAvailable') + }}
{{ t('traceDialog.model') }}: - {{ diagnosticModelId || '-' }} + {{ + diagnosticModelId || t('traceDialog.notAvailable') + }}
@@ -602,7 +606,7 @@ const resetState = () => { const formatNullable = (value: string | number | null): string => { if (value === null) { - return '-' + return t('traceDialog.notAvailable') } return String(value) } diff --git a/src/renderer/src/i18n/da-DK/traceDialog.json b/src/renderer/src/i18n/da-DK/traceDialog.json index 39790c8d2..b3c482176 100644 --- a/src/renderer/src/i18n/da-DK/traceDialog.json +++ b/src/renderer/src/i18n/da-DK/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Effektive maks. tokens", "reserveTokens": "Reserverede tokens", "toolReserveTokens": "Værktøjsreserverede tokens", - "estimatedPromptTokens": "Estimerede prompt-tokens" + "estimatedPromptTokens": "Estimerede prompt-tokens", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/de-DE/traceDialog.json b/src/renderer/src/i18n/de-DE/traceDialog.json index 1740a6d91..f23995bd8 100644 --- a/src/renderer/src/i18n/de-DE/traceDialog.json +++ b/src/renderer/src/i18n/de-DE/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Effektive maximale Tokens", "reserveTokens": "Reservierte Tokens", "toolReserveTokens": "Für Werkzeuge reservierte Tokens", - "estimatedPromptTokens": "Geschätzte Prompt-Tokens" + "estimatedPromptTokens": "Geschätzte Prompt-Tokens", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/en-US/traceDialog.json b/src/renderer/src/i18n/en-US/traceDialog.json index 0a0539cf6..054a0ee93 100644 --- a/src/renderer/src/i18n/en-US/traceDialog.json +++ b/src/renderer/src/i18n/en-US/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Effective max tokens", "reserveTokens": "Reserve tokens", "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "estimatedPromptTokens": "Estimated prompt tokens", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/es-ES/traceDialog.json b/src/renderer/src/i18n/es-ES/traceDialog.json index 386f59a7c..29dd6a1b9 100644 --- a/src/renderer/src/i18n/es-ES/traceDialog.json +++ b/src/renderer/src/i18n/es-ES/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Tokens máximos efectivos", "reserveTokens": "Tokens reservados", "toolReserveTokens": "Tokens reservados para herramientas", - "estimatedPromptTokens": "Tokens estimados del prompt" + "estimatedPromptTokens": "Tokens estimados del prompt", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/fa-IR/traceDialog.json b/src/renderer/src/i18n/fa-IR/traceDialog.json index 1842b674f..fe204d80f 100644 --- a/src/renderer/src/i18n/fa-IR/traceDialog.json +++ b/src/renderer/src/i18n/fa-IR/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "حداکثر توکن‌های مؤثر", "reserveTokens": "توکن‌های رزرو", "toolReserveTokens": "توکن‌های رزرو ابزار", - "estimatedPromptTokens": "توکن‌های تخمینی پرامپت" + "estimatedPromptTokens": "توکن‌های تخمینی پرامپت", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/fr-FR/traceDialog.json b/src/renderer/src/i18n/fr-FR/traceDialog.json index 6a0648b04..ddb6c143f 100644 --- a/src/renderer/src/i18n/fr-FR/traceDialog.json +++ b/src/renderer/src/i18n/fr-FR/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Tokens maximum effectifs", "reserveTokens": "Tokens réservés", "toolReserveTokens": "Tokens réservés aux outils", - "estimatedPromptTokens": "Tokens estimés du prompt" + "estimatedPromptTokens": "Tokens estimés du prompt", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/he-IL/traceDialog.json b/src/renderer/src/i18n/he-IL/traceDialog.json index fe3e56391..b5eea5bbc 100644 --- a/src/renderer/src/i18n/he-IL/traceDialog.json +++ b/src/renderer/src/i18n/he-IL/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "מספר טוקנים מרבי בפועל", "reserveTokens": "טוקנים שמורים", "toolReserveTokens": "טוקנים שמורים לכלים", - "estimatedPromptTokens": "טוקני פרומפט משוערים" + "estimatedPromptTokens": "טוקני פרומפט משוערים", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/id-ID/traceDialog.json b/src/renderer/src/i18n/id-ID/traceDialog.json index 486ae7b48..8bba21f16 100644 --- a/src/renderer/src/i18n/id-ID/traceDialog.json +++ b/src/renderer/src/i18n/id-ID/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Token maksimum efektif", "reserveTokens": "Token cadangan", "toolReserveTokens": "Token cadangan alat", - "estimatedPromptTokens": "Estimasi token prompt" + "estimatedPromptTokens": "Estimasi token prompt", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/it-IT/traceDialog.json b/src/renderer/src/i18n/it-IT/traceDialog.json index 3d02fc696..923345b0f 100644 --- a/src/renderer/src/i18n/it-IT/traceDialog.json +++ b/src/renderer/src/i18n/it-IT/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Token massimi effettivi", "reserveTokens": "Token riservati", "toolReserveTokens": "Token riservati agli strumenti", - "estimatedPromptTokens": "Token stimati del prompt" + "estimatedPromptTokens": "Token stimati del prompt", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/ja-JP/traceDialog.json b/src/renderer/src/i18n/ja-JP/traceDialog.json index dc5ecb558..ae3608410 100644 --- a/src/renderer/src/i18n/ja-JP/traceDialog.json +++ b/src/renderer/src/i18n/ja-JP/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "有効な最大トークン数", "reserveTokens": "予約トークン", "toolReserveTokens": "ツール予約トークン", - "estimatedPromptTokens": "推定プロンプトトークン数" + "estimatedPromptTokens": "推定プロンプトトークン数", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/ko-KR/traceDialog.json b/src/renderer/src/i18n/ko-KR/traceDialog.json index 00b7f0bd2..daad89ef3 100644 --- a/src/renderer/src/i18n/ko-KR/traceDialog.json +++ b/src/renderer/src/i18n/ko-KR/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "유효 최대 토큰", "reserveTokens": "예약 토큰", "toolReserveTokens": "도구 예약 토큰", - "estimatedPromptTokens": "예상 프롬프트 토큰" + "estimatedPromptTokens": "예상 프롬프트 토큰", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/ms-MY/traceDialog.json b/src/renderer/src/i18n/ms-MY/traceDialog.json index 165834146..551317483 100644 --- a/src/renderer/src/i18n/ms-MY/traceDialog.json +++ b/src/renderer/src/i18n/ms-MY/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Token maksimum berkesan", "reserveTokens": "Token simpanan", "toolReserveTokens": "Token simpanan alat", - "estimatedPromptTokens": "Anggaran token prompt" + "estimatedPromptTokens": "Anggaran token prompt", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/pl-PL/traceDialog.json b/src/renderer/src/i18n/pl-PL/traceDialog.json index c7c43668c..b9a66c0f7 100644 --- a/src/renderer/src/i18n/pl-PL/traceDialog.json +++ b/src/renderer/src/i18n/pl-PL/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Efektywne maks. tokeny", "reserveTokens": "Zarezerwowane tokeny", "toolReserveTokens": "Tokeny zarezerwowane dla narzędzi", - "estimatedPromptTokens": "Szacowane tokeny promptu" + "estimatedPromptTokens": "Szacowane tokeny promptu", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/pt-BR/traceDialog.json b/src/renderer/src/i18n/pt-BR/traceDialog.json index d6d94b205..2af639573 100644 --- a/src/renderer/src/i18n/pt-BR/traceDialog.json +++ b/src/renderer/src/i18n/pt-BR/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Máximo de tokens efetivo", "reserveTokens": "Tokens reservados", "toolReserveTokens": "Tokens reservados para ferramentas", - "estimatedPromptTokens": "Tokens estimados do prompt" + "estimatedPromptTokens": "Tokens estimados do prompt", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/ru-RU/traceDialog.json b/src/renderer/src/i18n/ru-RU/traceDialog.json index 173a7e9dc..73546b8e4 100644 --- a/src/renderer/src/i18n/ru-RU/traceDialog.json +++ b/src/renderer/src/i18n/ru-RU/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Фактический максимум токенов", "reserveTokens": "Зарезервированные токены", "toolReserveTokens": "Токены, зарезервированные для инструментов", - "estimatedPromptTokens": "Оценка токенов промпта" + "estimatedPromptTokens": "Оценка токенов промпта", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/tr-TR/traceDialog.json b/src/renderer/src/i18n/tr-TR/traceDialog.json index abed5187b..a662007d0 100644 --- a/src/renderer/src/i18n/tr-TR/traceDialog.json +++ b/src/renderer/src/i18n/tr-TR/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Geçerli en fazla token", "reserveTokens": "Ayrılmış tokenlar", "toolReserveTokens": "Araç için ayrılmış tokenlar", - "estimatedPromptTokens": "Tahmini prompt tokenları" + "estimatedPromptTokens": "Tahmini prompt tokenları", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/vi-VN/traceDialog.json b/src/renderer/src/i18n/vi-VN/traceDialog.json index 49fab3531..a245eabb8 100644 --- a/src/renderer/src/i18n/vi-VN/traceDialog.json +++ b/src/renderer/src/i18n/vi-VN/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "Số token tối đa hiệu dụng", "reserveTokens": "Token dự trữ", "toolReserveTokens": "Token dự trữ cho công cụ", - "estimatedPromptTokens": "Token prompt ước tính" + "estimatedPromptTokens": "Token prompt ước tính", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/zh-CN/traceDialog.json b/src/renderer/src/i18n/zh-CN/traceDialog.json index 2a3c43ed5..ca3c94a93 100644 --- a/src/renderer/src/i18n/zh-CN/traceDialog.json +++ b/src/renderer/src/i18n/zh-CN/traceDialog.json @@ -48,5 +48,6 @@ "effectiveMaxTokens": "生效 Max Tokens", "reserveTokens": "预留 Tokens", "toolReserveTokens": "工具预留 Tokens", - "estimatedPromptTokens": "估算 Prompt Tokens" + "estimatedPromptTokens": "估算 Prompt Tokens", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/zh-HK/traceDialog.json b/src/renderer/src/i18n/zh-HK/traceDialog.json index 87ebfaf09..9a64bec48 100644 --- a/src/renderer/src/i18n/zh-HK/traceDialog.json +++ b/src/renderer/src/i18n/zh-HK/traceDialog.json @@ -13,7 +13,7 @@ "notImplementedDesc": "此供應商暫時無法預覽", "provider": "供應商", "title": "請求參數預覽", - "model": "Model", + "model": "模型", "tabs": { "request": "要求", "view": "檢視", @@ -48,5 +48,6 @@ "effectiveMaxTokens": "實際最大 Tokens", "reserveTokens": "預留 Tokens", "toolReserveTokens": "工具預留 Tokens", - "estimatedPromptTokens": "估算提示詞 Tokens" + "estimatedPromptTokens": "估算提示詞 Tokens", + "notAvailable": "-" } diff --git a/src/renderer/src/i18n/zh-TW/traceDialog.json b/src/renderer/src/i18n/zh-TW/traceDialog.json index 6197aa0a4..67c34b793 100644 --- a/src/renderer/src/i18n/zh-TW/traceDialog.json +++ b/src/renderer/src/i18n/zh-TW/traceDialog.json @@ -13,7 +13,7 @@ "notImplementedDesc": "此供應商暫時無法預覽", "provider": "供應商", "title": "請求參數預覽", - "model": "Model", + "model": "模型", "tabs": { "request": "請求", "view": "檢視", @@ -48,5 +48,6 @@ "effectiveMaxTokens": "實際最大 Tokens", "reserveTokens": "保留 Tokens", "toolReserveTokens": "工具保留 Tokens", - "estimatedPromptTokens": "預估提示詞 Tokens" + "estimatedPromptTokens": "預估提示詞 Tokens", + "notAvailable": "-" } diff --git a/test/main/presenter/agentRuntimePresenter/tapeService.test.ts b/test/main/presenter/agentRuntimePresenter/tapeService.test.ts index b74b3b824..14fef0775 100644 --- a/test/main/presenter/agentRuntimePresenter/tapeService.test.ts +++ b/test/main/presenter/agentRuntimePresenter/tapeService.test.ts @@ -629,7 +629,10 @@ describe('DeepChatTapeService', () => { }) const manifestEntry = service.appendViewManifest(manifest) + const nowSpy = vi.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000) const slice = service.exportReplaySlice('s1', 'a1') + const secondSlice = service.exportReplaySlice('s1', 'a1') + nowSpy.mockRestore() expect(slice).toMatchObject({ schemaVersion: 1, @@ -647,6 +650,8 @@ describe('DeepChatTapeService', () => { } }) expect(slice?.hashes.sliceHash).toHaveLength(64) + expect(secondSlice?.hashes.sliceHash).toBe(slice?.hashes.sliceHash) + expect(secondSlice?.createdAt).toBe(2000) expect(slice?.trace?.bodyHash).toHaveLength(64) expect(slice?.trace?.bodyJson).toBeUndefined() expect(slice?.entries.some((entry) => entry.entryId === manifestEntry.entry_id)).toBe(true) diff --git a/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts b/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts index 5b430585a..c5a1dda65 100644 --- a/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts +++ b/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts @@ -109,6 +109,63 @@ describe('tapeViewManifest', () => { expect(JSON.stringify(first)).not.toContain('secret prompt content') }) + it('copies manifest refs so caller mutations cannot alter the hashed snapshot', () => { + const input = { + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + taskType: 'chat' as const, + policy: 'legacy_context_v1' as const, + policyVersion: 1, + messages: [{ role: 'user' as const, content: 'hello' }], + tools: [], + latestEntryId: 7, + anchorEntryIds: [1], + included: [ + { + entryId: 2, + messageId: 'u1', + orderSeq: 1, + role: 'user' as const, + source: 'tape' as const, + reason: 'selected_history' as const + } + ], + excluded: [ + { + entryId: 3, + messageId: 'u0', + orderSeq: 0, + role: 'user' as const, + source: 'tape' as const, + reason: 'out_of_budget' as const + } + ], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0 + }, + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: false, + assembledAt: 123 + } + + const manifest = createTapeViewManifest(input) + input.included[0].entryId = 99 + input.excluded[0].reason = 'empty_after_formatting' + + expect(manifest.included[0].entryId).toBe(2) + expect(manifest.excluded[0].reason).toBe('out_of_budget') + expect(manifest.hashes.manifestHash).not.toBe(createTapeViewManifest(input).hashes.manifestHash) + }) + it('resolves initial Tape policy provenance and request-level shadow policies', () => { expect( resolveTapeViewManifestPolicy({ diff --git a/test/renderer/api/clients.test.ts b/test/renderer/api/clients.test.ts index 1360bad53..32f38db3e 100644 --- a/test/renderer/api/clients.test.ts +++ b/test/renderer/api/clients.test.ts @@ -1291,6 +1291,147 @@ describe('renderer api clients', () => { }) }) + it('serializes settings save payloads before invoking the config bridge', async () => { + const bridge = createBridge() + const configClient = createConfigClient(bridge) + const shortcutKeys = new Proxy( + { + toggleWindow: 'CommandOrControl+K' + }, + {} + ) + const promptParameters = new Proxy( + [ + { + name: 'topic', + description: 'Topic', + required: true + } + ], + {} + ) + const customPrompt = new Proxy( + { + id: 'prompt-1', + name: 'Writer', + description: 'Write clearly', + content: 'Write about {{topic}}', + parameters: promptParameters, + enabled: true + }, + {} + ) + const customPromptUpdate = new Proxy( + { + description: 'Updated', + parameters: promptParameters + }, + {} + ) + const systemPrompt = new Proxy( + { + id: 'system-1', + name: 'System', + content: 'Be concise' + }, + {} + ) + const modelSelection = new Proxy( + { + providerId: 'openai', + modelId: 'gpt-4.1' + }, + {} + ) + const deepChatAgentInput = new Proxy( + { + name: 'Writer Agent', + enabled: true, + config: new Proxy( + { + assistantModel: modelSelection + }, + {} + ) + }, + {} + ) + + await configClient.setShortcutKey(shortcutKeys) + await configClient.addCustomPrompt(customPrompt) + await configClient.updateCustomPrompt('prompt-1', customPromptUpdate) + await configClient.setCustomPrompts(new Proxy([customPrompt], {})) + await configClient.addSystemPrompt(systemPrompt) + await configClient.updateSystemPrompt('system-1', new Proxy({ content: 'Updated' }, {})) + await configClient.setSystemPrompts(new Proxy([systemPrompt], {})) + await configClient.createDeepChatAgent(deepChatAgentInput) + await configClient.updateDeepChatAgent( + 'writer', + new Proxy( + { + config: new Proxy( + { + visionModel: modelSelection + }, + {} + ) + }, + {} + ) + ) + + const calls = (bridge.invoke as ReturnType).mock.calls + for (const [, payload] of calls) { + expect(() => structuredClone(payload)).not.toThrow() + } + + expect(calls[0]).toEqual([ + 'config.setShortcutKeys', + { + shortcuts: { + toggleWindow: 'CommandOrControl+K' + } + } + ]) + expect(calls[1][1].prompt).toEqual({ + id: 'prompt-1', + name: 'Writer', + description: 'Write clearly', + content: 'Write about {{topic}}', + parameters: [ + { + name: 'topic', + description: 'Topic', + required: true + } + ], + enabled: true + }) + expect(calls[1][1].prompt).not.toBe(customPrompt) + expect(calls[1][1].prompt.parameters).not.toBe(promptParameters) + expect(calls[7][1]).toEqual({ + name: 'Writer Agent', + enabled: true, + config: { + assistantModel: { + providerId: 'openai', + modelId: 'gpt-4.1' + } + } + }) + expect(calls[8][1]).toEqual({ + agentId: 'writer', + updates: { + config: { + visionModel: { + providerId: 'openai', + modelId: 'gpt-4.1' + } + } + } + }) + }) + it('routes ACP config calls through the shared registry names', async () => { const bridge = createBridge() const configClient = createConfigClient(bridge) diff --git a/test/renderer/components/DeepChatAgentsSettings.test.ts b/test/renderer/components/DeepChatAgentsSettings.test.ts index e7f7cb1e9..74fd4a6ee 100644 --- a/test/renderer/components/DeepChatAgentsSettings.test.ts +++ b/test/renderer/components/DeepChatAgentsSettings.test.ts @@ -127,7 +127,7 @@ describe('DeepChatAgentsSettings', () => { clientMocks.toolClient.getAllToolDefinitions.mockReset() }) - it('mounts and saves DeepChat agents without advanced model overrides', async () => { + it('mounts and saves DeepChat agents with cloneable model selections', async () => { vi.resetModules() const existingAgent = { @@ -150,8 +150,8 @@ describe('DeepChatAgentsSettings', () => { verbosity: 'high', forceInterleavedThinkingCompat: true }, - assistantModel: null, - visionModel: null, + assistantModel: { providerId: 'anthropic', modelId: 'claude-3-5-sonnet' }, + visionModel: { providerId: 'openai', modelId: 'gpt-4.1-vision' }, imageGenerationModel: { providerId: 'openai', modelId: 'gpt-image-1' }, systemPrompt: 'system prompt', permissionMode: 'default', @@ -194,8 +194,13 @@ describe('DeepChatAgentsSettings', () => { providerId: 'openai', models: [ { id: 'gpt-4.1', name: 'GPT-4.1' }, + { id: 'gpt-4.1-vision', name: 'GPT-4.1 Vision' }, { id: 'gpt-image-1', name: 'GPT Image 1', type: ModelType.ImageGeneration } ] + }, + { + providerId: 'anthropic', + models: [{ id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet' }] } ], findModelByIdOrName: vi.fn((modelId: string) => @@ -300,8 +305,8 @@ describe('DeepChatAgentsSettings', () => { providerId: 'openai', modelId: 'gpt-4.1' }, - assistantModel: null, - visionModel: null, + assistantModel: { providerId: 'anthropic', modelId: 'claude-3-5-sonnet' }, + visionModel: { providerId: 'openai', modelId: 'gpt-4.1-vision' }, imageGenerationModel: { providerId: 'openai', modelId: 'gpt-image-1' }, defaultProjectPath: null, systemPrompt: 'system prompt', @@ -316,10 +321,19 @@ describe('DeepChatAgentsSettings', () => { providerId: 'openai', modelId: 'gpt-4.1' }) + expect(payload.config.assistantModel).toEqual({ + providerId: 'anthropic', + modelId: 'claude-3-5-sonnet' + }) + expect(payload.config.visionModel).toEqual({ + providerId: 'openai', + modelId: 'gpt-4.1-vision' + }) expect(payload.config.imageGenerationModel).toEqual({ providerId: 'openai', modelId: 'gpt-image-1' }) + expect(() => structuredClone(payload)).not.toThrow() }) it('filters the image generation model selector to image models', async () => { diff --git a/test/renderer/components/McpServerForm.test.ts b/test/renderer/components/McpServerForm.test.ts new file mode 100644 index 000000000..d84fa5104 --- /dev/null +++ b/test/renderer/components/McpServerForm.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it, vi } from 'vitest' +import { defineComponent } from 'vue' +import { mount } from '@vue/test-utils' + +const passthrough = (name: string, tag = 'div') => + defineComponent({ + name, + template: `<${tag} v-bind="$attrs">` + }) + +const buttonStub = defineComponent({ + name: 'Button', + emits: ['click'], + template: + '' +}) + +const inputStub = defineComponent({ + name: 'Input', + props: { + modelValue: { type: [String, Number], default: '' } + }, + emits: ['update:modelValue'], + template: + '' +}) + +const textareaStub = defineComponent({ + name: 'Textarea', + props: { + modelValue: { type: String, default: '' } + }, + emits: ['update:modelValue'], + template: + '