dsh-model-switch 0.4.18 → 0.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/PRODUCT.md +1 -1
- package/README.md +2 -0
- package/README.zh.md +2 -0
- package/lib/client.js +136 -14
- package/lib/types/client/picker/PlanReviewCard.d.ts +3 -0
- package/lib/types/client/picker/locales.d.ts +8 -0
- package/lib/types/picker/plan-goal.d.ts +77 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## v0.4.19
|
|
2
|
+
|
|
3
|
+
Plan Review gains a **Set as goal and run** action: it arms the reviewed Plan as the session's goal and only then answers it, so `dsh-goal-round-driver` carries the Plan after plan mode exits.
|
|
4
|
+
|
|
5
|
+
- The goal is set before the answer, matching the card's existing commit-then-answer order; a refusal keeps the review pending and reports the Host's own wire code.
|
|
6
|
+
- An unfinished goal is replaced through `get` + `clear` + `create` rather than edited, so a new Plan starts with a fresh round cap, and the card says it replaced the previous goal.
|
|
7
|
+
- The goal face is resolved per render (`ctx.get('remote.goals', false)`) instead of listed in the static client `inject`, so a deployment that mounts no `@deepseek-ai/dsh-goal` keeps loading this plugin and the action disables itself with an explicit reason while the other three actions keep working. `@deepseek-ai/dsh-goal` is declared as an optional peer.
|
|
8
|
+
- The objective is the Plan's trimmed markdown. The plugin stores no goal state, owns no part of the goal domain, and schedules no rounds.
|
|
9
|
+
- Records the seam in `docs/seam-gap.md` (the released `remote.goals.create` exists while DSH's own goal surface deliberately omits creation) and the action in PRODUCT.md and both READMEs.
|
|
10
|
+
|
|
1
11
|
## v0.4.18
|
|
2
12
|
|
|
3
13
|
Declare official DSH `0.2.0-rc.2` compatible in `package.json#dsh.compatibility.dshReleases`.
|
package/PRODUCT.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- **Vision:** excluded. Model Switch exposes no Vision setting, does not shadow `read_image`, and does not preprocess chat attachments.
|
|
11
11
|
- **Image Router:** owns a new, uniquely named stable `generate_image` tool. It selects the configured Codex or Grok adapter/model at execution time, rejects backend-incompatible fields, and returns normalized image metadata. Existing provider-specific image tools remain available as rollback paths.
|
|
12
12
|
- **Composer Picker:** replaces the composer model seat with a suffix-grouped Model / Effort / Context / Fast / Thinking picker and submits through the official Model Directory. Variant changes preserve an effort only when the target catalog row supports it, otherwise they use the target default or omit effort. Picker changes are session-local. A submitted prompt reserves the selected runtime immediately (composer submitting, pending first turn, or running). Native Antigravity keeps its models and effort; DSH keeps LLM-provider choices and disables Agent providers. After a successful native binding, other groups stay disabled. Cancel without a binding releases the reservation; existing DSH history still cannot convert onto an External Agent. On a Host version that also persists session selection as the deployment default, the plugin restores the pre-switch Main default through the public Settings RPC with a revision fence; a concurrent Settings-page edit wins.
|
|
13
|
-
- **Plan Review:** intercepts the official `plan-review` composer takeover with a header execution-model picker and right-aligned Discuss in chat / Reject / Confirm actions. Confirm commits the selected model before answering; a refused commit leaves the review pending and retryable, while a deployment default this plugin could not restore is announced after the answer instead of holding the human's decision back. Reject carries optional human feedback as the answer's own `custom` text, which plan mode reports to the model. Discuss in chat withdraws the takeover through the Host's own verb — `dismiss()` where the Host publishes it, `cancel()` on the older generation — and names the Host when neither seam exists, so a settled review is never a dead end. The same runtime reservation applies.
|
|
13
|
+
- **Plan Review:** intercepts the official `plan-review` composer takeover with a header execution-model picker and right-aligned Discuss in chat / Reject / Set as goal and run / Confirm actions. Confirm commits the selected model before answering; a refused commit leaves the review pending and retryable, while a deployment default this plugin could not restore is announced after the answer instead of holding the human's decision back. Reject carries optional human feedback as the answer's own `custom` text, which plan mode reports to the model. **Set as goal and run** arms this Plan as the session's goal through the released `remote.goals.create` — replacing an unfinished goal with `get` + `clear` first so the new Plan receives a fresh round cap — and only then answers, so `dsh-goal-round-driver` carries the Plan after plan mode exits. The goal is set before the answer, a refusal keeps the review pending and retryable, and a deployment that mounts no goal service disables the action with an explicit reason; the plugin stores no goal state, schedules no rounds, and owns no part of the goal domain. Discuss in chat withdraws the takeover through the Host's own verb — `dismiss()` where the Host publishes it, `cancel()` on the older generation — and names the Host when neither seam exists, so a settled review is never a dead end. The same runtime reservation applies.
|
|
14
14
|
|
|
15
15
|
Model choices are capability-driven. Invalid, unavailable, or unsupported selections remain visible to configuration surfaces and fail loudly at use; routing never silently falls back.
|
|
16
16
|
|
package/README.md
CHANGED
|
@@ -79,6 +79,8 @@ Plan Review owns an execution-model draft separate from Main. **Confirm** first
|
|
|
79
79
|
|
|
80
80
|
**Reject** sends the option the asker offered, and any text typed in the card's feedback field as the answer's own `custom` text — plan mode reports that text back to the model as the reason to keep planning, so a rejection can carry what to change. **Discuss in chat** withdraws the takeover so the composer returns and the human speaks in their own words; it uses the Host's `dismiss()` where published and the older generation's `cancel()` otherwise, and names the Host when neither exists. Answering a settled review closes Confirm and Reject, while Discuss and the reload affordance stay reachable so the card always has a way back. Failures keep their wire code (`session/writer-held: …`) visible instead of a bare localized sentence.
|
|
81
81
|
|
|
82
|
+
**Set as goal and run** arms this Plan as the session's goal and only then answers it, so `dsh-goal-round-driver` keeps the Plan moving after plan mode exits. It calls the released `remote.goals.create`; when a non-complete goal already exists it clears that one first so the new Plan starts with a fresh round cap, and says so in the card. A refusal keeps the review pending with its wire code (`GOAL_AGENT_NOT_LIVE: …`), and a deployment that mounts no goal service disables the button with an explicit reason while the other three actions keep working. The objective is the Plan's full markdown, trimmed — the goal domain imposes no length limit, and the objective rides along as model-visible context every round, so a long Plan costs tokens on each continuation.
|
|
83
|
+
|
|
82
84
|

|
|
83
85
|
|
|
84
86
|
## Send-time context protection
|
package/README.zh.md
CHANGED
|
@@ -77,6 +77,8 @@ Plan Review 拥有独立于 Main 的执行模型草稿。**确认执行**会先
|
|
|
77
77
|
|
|
78
78
|
**拒绝**会发送提问方给出的选项,并把卡片反馈框里的文字作为答复自身的 `custom` 文本一并提交 —— plan 模式会把它作为"继续规划"的理由回报给模型,所以打回时可以带上要改什么。**去聊天里说**会撤下这张卡、把输入框还给你,用你自己的话说明;它在 Host 提供 `dismiss()` 时用它,在旧代际用 `cancel()`,两者都没有时明确指出这一点。审核一旦落定,**确认执行**与**拒绝**关闭,而**去聊天里说**与"重新加载"始终可达,卡片不会变成死路。失败会保留线上错误码(如 `session/writer-held: …`),而不是只给一句本地化文案。
|
|
79
79
|
|
|
80
|
+
**设置为目标并执行** 会先把这份 Plan 设为当前会话的 goal、再答复,于是 `dsh-goal-round-driver` 能在退出计划模式后继续推进。它调用已发布的 `remote.goals.create`;若已存在一个未完成的 goal,会先 `clear` 再 `create`,让新 Plan 拿到完整轮次预算,并在替换后提示。目标设置被拒时审核保持待处理、错误带真实错误码(如 `GOAL_AGENT_NOT_LIVE: …`);部署未挂 goal 服务时该按钮禁用并给出原因,其余三个动作照常可用。objective 取计划全文(trim 后):goal 域不设长度上限,但它会作为模型可见上下文出现在每一轮续跑里,所以计划很长时每轮都花 token。
|
|
81
|
+
|
|
80
82
|

|
|
81
83
|
|
|
82
84
|
## 发送时的上下文保护
|
package/lib/client.js
CHANGED
|
@@ -3063,6 +3063,63 @@ window.__ModuleLoader__.load({
|
|
|
3063
3063
|
throw new PlanDismissalUnsupportedError("this DSH Host exposes neither PendingQuestion.dismiss() (0.2.x) nor PendingQuestion.cancel() (0.1.x)");
|
|
3064
3064
|
}
|
|
3065
3065
|
//#endregion
|
|
3066
|
+
//#region src/picker/plan-goal.ts
|
|
3067
|
+
function failure(result, fallback) {
|
|
3068
|
+
if (result.ok) return { message: fallback };
|
|
3069
|
+
const { code, message } = result.error;
|
|
3070
|
+
return {
|
|
3071
|
+
code,
|
|
3072
|
+
message: message.trim() === "" ? fallback : message
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Make the plan text this session's armed goal, replacing any unfinished goal.
|
|
3077
|
+
*
|
|
3078
|
+
* Ordering mirrors the card's own commit-then-answer rule: a refusal here keeps
|
|
3079
|
+
* the Plan pending instead of approving work that no goal tracks. The replace
|
|
3080
|
+
* path clears rather than edits so a new plan receives a fresh round budget —
|
|
3081
|
+
* `edit` would inherit the rounds the previous goal already spent.
|
|
3082
|
+
*
|
|
3083
|
+
* @param remote - the released goal remote, absent when no goal package is mounted.
|
|
3084
|
+
* @param sessionId - the session that owns both the review and the goal.
|
|
3085
|
+
* @param plan - the plan markdown under review.
|
|
3086
|
+
* @returns Whether the goal now tracks this plan, or why it does not.
|
|
3087
|
+
*/
|
|
3088
|
+
async function setPlanAsGoal(remote, sessionId, plan) {
|
|
3089
|
+
if (remote === void 0) return { kind: "unsupported" };
|
|
3090
|
+
const objective = plan.trim();
|
|
3091
|
+
if (objective === "") return {
|
|
3092
|
+
kind: "failed",
|
|
3093
|
+
code: "GOAL_INVALID_OBJECTIVE",
|
|
3094
|
+
message: "goal objective must be a non-empty string"
|
|
3095
|
+
};
|
|
3096
|
+
const created = await remote.create(sessionId, { objective });
|
|
3097
|
+
if (created.ok) return { kind: "created" };
|
|
3098
|
+
if (created.error.code !== "GOAL_ALREADY_EXISTS") return {
|
|
3099
|
+
kind: "failed",
|
|
3100
|
+
...failure(created, "the goal could not be created")
|
|
3101
|
+
};
|
|
3102
|
+
const current = await remote.get(sessionId);
|
|
3103
|
+
if (!current.ok || current.value === void 0 || current.value === null) return {
|
|
3104
|
+
kind: "failed",
|
|
3105
|
+
...failure(current, "the current goal could not be read for replacement")
|
|
3106
|
+
};
|
|
3107
|
+
const cleared = await remote.clear(sessionId, {
|
|
3108
|
+
id: current.value.id,
|
|
3109
|
+
revision: current.value.revision
|
|
3110
|
+
});
|
|
3111
|
+
if (!cleared.ok) return {
|
|
3112
|
+
kind: "failed",
|
|
3113
|
+
...failure(cleared, "the current goal could not be cleared")
|
|
3114
|
+
};
|
|
3115
|
+
const retry = await remote.create(sessionId, { objective });
|
|
3116
|
+
if (retry.ok) return { kind: "replaced" };
|
|
3117
|
+
return {
|
|
3118
|
+
kind: "failed",
|
|
3119
|
+
...failure(retry, "the goal could not be created")
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
//#endregion
|
|
3066
3123
|
//#region src/client/picker/RetryBoundary.tsx
|
|
3067
3124
|
/** Own retryable React error-boundary state while callers own presentation and copy. */
|
|
3068
3125
|
var RetryBoundary = class extends react.Component {
|
|
@@ -3082,7 +3139,7 @@ window.__ModuleLoader__.load({
|
|
|
3082
3139
|
};
|
|
3083
3140
|
//#endregion
|
|
3084
3141
|
//#region \0dsh-css:src/client/picker/PlanReviewCard.module.css.mjs
|
|
3085
|
-
const css = ".BylfoW_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.BylfoW_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-state-warn-secondary);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;display:flex;overflow:visible}.BylfoW_card,.BylfoW_card *{box-sizing:border-box}.BylfoW_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);border-radius:20px 20px 0 0;flex-shrink:0;align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.BylfoW_dot{background:var(--dsw-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.BylfoW_stripTitle{flex:none;font-weight:600}.BylfoW_headerPicker{flex:0 auto;align-items:center;min-width:0;max-width:55%;margin-left:auto;display:flex}.BylfoW_headerPicker>*{min-width:0;max-width:100%}.BylfoW_body{overscroll-behavior:contain;flex:auto;min-height:0;padding:12px 16px 4px;font-size:14px;line-height:22px;overflow-y:auto}.BylfoW_footer{z-index:2;flex-direction:column;flex-shrink:0;gap:6px;padding:8px 16px 12px;display:flex;position:relative;overflow:visible}.BylfoW_feedback{min-height:0;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}.BylfoW_feedback:empty{display:none}.BylfoW_feedbackRow{align-items:center;gap:8px;min-width:0;display:flex}.BylfoW_feedbackInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-input-minor);min-width:0;height:32px;color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13);border-radius:8px;flex:auto;padding:0 10px}.BylfoW_feedbackInput::placeholder{color:var(--dsw-alias-label-tertiary)}.BylfoW_feedbackInput:disabled{opacity:.6}.BylfoW_bar{flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:12px;min-width:0;min-height:32px;display:flex}.BylfoW_picker{flex:16rem;min-width:0;max-width:16rem}.BylfoW_pickerError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.BylfoW_actions{flex-wrap:nowrap;flex:none;justify-content:flex-end;align-items:center;gap:8px;margin-left:auto;display:flex}.BylfoW_discuss,.BylfoW_keep,.BylfoW_approve,.BylfoW_recover{white-space:nowrap;height:32px;min-height:32px;padding:0 10px}.BylfoW_discuss,.BylfoW_recover{color:var(--dsw-alias-label-secondary)}.BylfoW_discuss:hover:not(:disabled),.BylfoW_recover:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BylfoW_approve{padding:0 12px}@media (width<=720px){.BylfoW_frame{padding-right:max(10px, env(safe-area-inset-right));padding-bottom:max(10px, env(safe-area-inset-bottom));padding-left:max(10px, env(safe-area-inset-left))}.BylfoW_card{border-radius:16px}.BylfoW_strip,.BylfoW_body,.BylfoW_footer{padding-left:12px;padding-right:12px}.BylfoW_strip{flex-wrap:wrap;gap:6px}.BylfoW_headerPicker{flex:100%;max-width:100%;margin-left:0}.BylfoW_bar{justify-content:flex-end;gap:6px}.BylfoW_actions{flex:100%;justify-content:flex-end;gap:6px;min-width:0}.BylfoW_discuss,.BylfoW_keep,.BylfoW_approve,.BylfoW_recover{overflow-wrap:anywhere;white-space:normal;flex:1 1 0;min-width:0;height:auto;padding:4px;line-height:14px}.BylfoW_feedbackInput{height:auto;min-height:32px}}";
|
|
3142
|
+
const css = ".BylfoW_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.BylfoW_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-state-warn-secondary);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;display:flex;overflow:visible}.BylfoW_card,.BylfoW_card *{box-sizing:border-box}.BylfoW_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);border-radius:20px 20px 0 0;flex-shrink:0;align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.BylfoW_dot{background:var(--dsw-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.BylfoW_stripTitle{flex:none;font-weight:600}.BylfoW_headerPicker{flex:0 auto;align-items:center;min-width:0;max-width:55%;margin-left:auto;display:flex}.BylfoW_headerPicker>*{min-width:0;max-width:100%}.BylfoW_body{overscroll-behavior:contain;flex:auto;min-height:0;padding:12px 16px 4px;font-size:14px;line-height:22px;overflow-y:auto}.BylfoW_footer{z-index:2;flex-direction:column;flex-shrink:0;gap:6px;padding:8px 16px 12px;display:flex;position:relative;overflow:visible}.BylfoW_feedback{min-height:0;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}.BylfoW_feedback:empty{display:none}.BylfoW_feedbackRow{align-items:center;gap:8px;min-width:0;display:flex}.BylfoW_feedbackInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-input-minor);min-width:0;height:32px;color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13);border-radius:8px;flex:auto;padding:0 10px}.BylfoW_feedbackInput::placeholder{color:var(--dsw-alias-label-tertiary)}.BylfoW_feedbackInput:disabled{opacity:.6}.BylfoW_bar{flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:12px;min-width:0;min-height:32px;display:flex}.BylfoW_picker{flex:16rem;min-width:0;max-width:16rem}.BylfoW_pickerError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.BylfoW_actions{flex-wrap:nowrap;flex:none;justify-content:flex-end;align-items:center;gap:8px;margin-left:auto;display:flex}.BylfoW_discuss,.BylfoW_keep,.BylfoW_goalRun,.BylfoW_approve,.BylfoW_recover{white-space:nowrap;height:32px;min-height:32px;padding:0 10px}.BylfoW_discuss,.BylfoW_recover{color:var(--dsw-alias-label-secondary)}.BylfoW_discuss:hover:not(:disabled),.BylfoW_recover:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BylfoW_approve{padding:0 12px}@media (width<=720px){.BylfoW_frame{padding-right:max(10px, env(safe-area-inset-right));padding-bottom:max(10px, env(safe-area-inset-bottom));padding-left:max(10px, env(safe-area-inset-left))}.BylfoW_card{border-radius:16px}.BylfoW_strip,.BylfoW_body,.BylfoW_footer{padding-left:12px;padding-right:12px}.BylfoW_strip{flex-wrap:wrap;gap:6px}.BylfoW_headerPicker{flex:100%;max-width:100%;margin-left:0}.BylfoW_bar{justify-content:flex-end;gap:6px}.BylfoW_actions{flex:100%;justify-content:flex-end;gap:6px;min-width:0}.BylfoW_discuss,.BylfoW_keep,.BylfoW_goalRun,.BylfoW_approve,.BylfoW_recover{overflow-wrap:anywhere;white-space:normal;flex:1 1 0;min-width:0;height:auto;padding:4px;line-height:14px}.BylfoW_feedbackInput{height:auto;min-height:32px}}";
|
|
3086
3143
|
const tagId = "dsh-model-switch/PlanReviewCard.module.css";
|
|
3087
3144
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
3088
3145
|
const tag = document.createElement("style");
|
|
@@ -3104,6 +3161,7 @@ window.__ModuleLoader__.load({
|
|
|
3104
3161
|
"feedbackRow": "BylfoW_feedbackRow",
|
|
3105
3162
|
"footer": "BylfoW_footer",
|
|
3106
3163
|
"frame": "BylfoW_frame",
|
|
3164
|
+
"goalRun": "BylfoW_goalRun",
|
|
3107
3165
|
"headerPicker": "BylfoW_headerPicker",
|
|
3108
3166
|
"keep": "BylfoW_keep",
|
|
3109
3167
|
"picker": "BylfoW_picker",
|
|
@@ -3226,11 +3284,12 @@ window.__ModuleLoader__.load({
|
|
|
3226
3284
|
...settingsUnavailableReason === void 0 ? {} : { settingsUnavailableReason },
|
|
3227
3285
|
directory: pickerDirectoryViewOrdered(snapshot, props, order, props.catalogRoutes?.() ?? {}),
|
|
3228
3286
|
t: props.t,
|
|
3287
|
+
goalRemote: props.goalRemote,
|
|
3229
3288
|
...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations },
|
|
3230
3289
|
...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
|
|
3231
3290
|
}, props.matched.key);
|
|
3232
3291
|
}
|
|
3233
|
-
function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, resolveInteractionOperations, roleOf }) {
|
|
3292
|
+
function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, resolveInteractionOperations, roleOf }) {
|
|
3234
3293
|
const { snapshot, getDirectorySnapshot, load, select } = directory;
|
|
3235
3294
|
const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
|
|
3236
3295
|
const [feedback, setFeedback] = (0, react.useState)("");
|
|
@@ -3280,22 +3339,45 @@ window.__ModuleLoader__.load({
|
|
|
3280
3339
|
blocked,
|
|
3281
3340
|
error
|
|
3282
3341
|
}, available, executionAllowed);
|
|
3342
|
+
/** Approve this review after committing its execution model; a refused commit keeps the Plan pending. */
|
|
3343
|
+
const approveWith = async (model) => {
|
|
3344
|
+
if (!await approvePlanReview({
|
|
3345
|
+
select,
|
|
3346
|
+
selection: model,
|
|
3347
|
+
current: snapshot.current,
|
|
3348
|
+
answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true),
|
|
3349
|
+
onNotRestored: (switched) => {
|
|
3350
|
+
setNotice(t("plan.notRestored", { model: switched }));
|
|
3351
|
+
console.warn("dsh-model-switch: the Session model changed but the Main default was not restored", switched);
|
|
3352
|
+
}
|
|
3353
|
+
})) {
|
|
3354
|
+
const message = getDirectorySnapshot().error;
|
|
3355
|
+
throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3283
3358
|
const onApprove = () => {
|
|
3284
3359
|
if (execution === void 0 || !executionAllowed || !available || busy || blocked) return;
|
|
3360
|
+
settle(() => approveWith(execution));
|
|
3361
|
+
};
|
|
3362
|
+
const goal = goalRemote();
|
|
3363
|
+
const goalReason = goal === void 0 ? t("plan.goalUnsupported") : void 0;
|
|
3364
|
+
/**
|
|
3365
|
+
* Arm this plan as the session's goal first, then approve. The goal must be
|
|
3366
|
+
* set before the answer because a refusal must leave the Plan pending, and
|
|
3367
|
+
* because only an armed goal lets the round driver carry the plan after plan
|
|
3368
|
+
* mode exits.
|
|
3369
|
+
*/
|
|
3370
|
+
const onRunAsGoal = () => {
|
|
3371
|
+
if (execution === void 0 || !executionAllowed || !available || busy || blocked || goal === void 0) return;
|
|
3285
3372
|
settle(async () => {
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
onNotRestored: (model) => {
|
|
3292
|
-
setNotice(t("plan.notRestored", { model }));
|
|
3293
|
-
console.warn("dsh-model-switch: the Session model changed but the Main default was not restored", model);
|
|
3294
|
-
}
|
|
3295
|
-
})) {
|
|
3296
|
-
const message = getDirectorySnapshot().error;
|
|
3297
|
-
throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
|
|
3373
|
+
const handoff = await setPlanAsGoal(goal, matched.sessionId, review.plan);
|
|
3374
|
+
if (handoff.kind === "unsupported") throw new Error(t("plan.goalUnsupported"));
|
|
3375
|
+
if (handoff.kind === "failed") {
|
|
3376
|
+
const message = handoff.code === void 0 ? handoff.message : `${handoff.code}: ${handoff.message}`;
|
|
3377
|
+
throw new Error(t("plan.goalFailed", { message }));
|
|
3298
3378
|
}
|
|
3379
|
+
if (handoff.kind === "replaced") setNotice(t("plan.goalReplaced"));
|
|
3380
|
+
await approveWith(execution);
|
|
3299
3381
|
});
|
|
3300
3382
|
};
|
|
3301
3383
|
const decline = review.decline;
|
|
@@ -3413,6 +3495,14 @@ window.__ModuleLoader__.load({
|
|
|
3413
3495
|
},
|
|
3414
3496
|
children: t("plan.keep")
|
|
3415
3497
|
}),
|
|
3498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3499
|
+
variant: "outline",
|
|
3500
|
+
className: PlanReviewCard_module_css_default.goalRun,
|
|
3501
|
+
disabled: action.approveDisabled || goalReason !== void 0,
|
|
3502
|
+
...goalReason === void 0 ? {} : { title: goalReason },
|
|
3503
|
+
onClick: onRunAsGoal,
|
|
3504
|
+
children: t("plan.goalRun")
|
|
3505
|
+
}),
|
|
3416
3506
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3417
3507
|
variant: "primary",
|
|
3418
3508
|
className: PlanReviewCard_module_css_default.approve,
|
|
@@ -3501,6 +3591,10 @@ window.__ModuleLoader__.load({
|
|
|
3501
3591
|
"plan.feedbackPlaceholder": "说明要改什么(可留空)",
|
|
3502
3592
|
"plan.notRestored": "执行模型已切换,但主模型默认值未能还原({model});请在「设置 → 模型」中确认默认模型。",
|
|
3503
3593
|
"plan.dismissUnsupported": "当前 DSH 版本既未提供 PendingQuestion.dismiss() 也未提供 cancel(),无法回到聊天;请直接在输入框里发送你的意见。",
|
|
3594
|
+
"plan.goalRun": "设置为目标并执行",
|
|
3595
|
+
"plan.goalFailed": "设置目标失败:{message}",
|
|
3596
|
+
"plan.goalReplaced": "已替换本会话原有的目标",
|
|
3597
|
+
"plan.goalUnsupported": "当前部署未挂载 goal 服务,无法把计划设为目标",
|
|
3504
3598
|
"plan.responseRejected": "计划答复已被另一客户端处理;已提交的模型切换无法由插件回滚。",
|
|
3505
3599
|
"plan.cancelRejected": "计划审查已被另一客户端处理,无法返回讨论。",
|
|
3506
3600
|
"markdown.copy": "复制代码",
|
|
@@ -3552,6 +3646,10 @@ window.__ModuleLoader__.load({
|
|
|
3552
3646
|
"plan.feedbackPlaceholder": "Say what to change (optional)",
|
|
3553
3647
|
"plan.notRestored": "The execution model switched, but the Main default could not be restored ({model}); confirm the default in Settings → Models.",
|
|
3554
3648
|
"plan.dismissUnsupported": "This DSH Host exposes neither PendingQuestion.dismiss() nor cancel(), so the review cannot return to chat; send your feedback as a message instead.",
|
|
3649
|
+
"plan.goalRun": "Set as goal and run",
|
|
3650
|
+
"plan.goalFailed": "Could not set the goal: {message}",
|
|
3651
|
+
"plan.goalReplaced": "Replaced this session's previous goal",
|
|
3652
|
+
"plan.goalUnsupported": "This deployment mounts no goal service, so the Plan cannot be set as a goal",
|
|
3555
3653
|
"plan.responseRejected": "Another client already handled this Plan response. A committed model change cannot be rolled back by this plugin.",
|
|
3556
3654
|
"plan.cancelRejected": "Another client already handled this Plan review, so it cannot return to discussion.",
|
|
3557
3655
|
"markdown.copy": "Copy code",
|
|
@@ -3576,6 +3674,29 @@ window.__ModuleLoader__.load({
|
|
|
3576
3674
|
}
|
|
3577
3675
|
const EMPTY_ORDER = [];
|
|
3578
3676
|
/**
|
|
3677
|
+
* Resolve the released goal remote without hard-depending on it.
|
|
3678
|
+
*
|
|
3679
|
+
* `remote.goals` exists only when the deployment mounts `@deepseek-ai/dsh-goal`,
|
|
3680
|
+
* so the capability must be looked up per render rather than injected: an
|
|
3681
|
+
* `inject` on a dotted service name would otherwise block this whole client
|
|
3682
|
+
* plugin on a deployment that does not mount the goal package. The dotted name
|
|
3683
|
+
* is read first, with the namespace object as the fallback shape.
|
|
3684
|
+
* @param ctx - client root context holding the remote namespace.
|
|
3685
|
+
* @returns The goal remote, or undefined when no goal service is mounted.
|
|
3686
|
+
*/
|
|
3687
|
+
function goalRemoteFrom(ctx) {
|
|
3688
|
+
const read = (name) => {
|
|
3689
|
+
try {
|
|
3690
|
+
return ctx.get(name, false);
|
|
3691
|
+
} catch {
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
};
|
|
3695
|
+
const nested = read("remote.goals");
|
|
3696
|
+
if (nested !== void 0 && nested !== null) return nested;
|
|
3697
|
+
return read("remote")?.goals;
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3579
3700
|
* Bind the optional Providers entry order as a React external store.
|
|
3580
3701
|
* @returns A subscribable order snapshot with an invalidation hook for directory changes.
|
|
3581
3702
|
*/
|
|
@@ -3709,6 +3830,7 @@ window.__ModuleLoader__.load({
|
|
|
3709
3830
|
},
|
|
3710
3831
|
subscribeMainDefaults: (listener) => mainDefaults.subscribe(listener),
|
|
3711
3832
|
getMainDefaultsSnapshot: () => mainDefaults.getSnapshot(),
|
|
3833
|
+
goalRemote: () => goalRemoteFrom(scope),
|
|
3712
3834
|
hooks: {
|
|
3713
3835
|
directory: directory.store,
|
|
3714
3836
|
providerOrder: orderStore
|
|
@@ -2,6 +2,7 @@ import type { PendingQuestion } from '@deepseek-ai/dsh-client-ui-user-questions/
|
|
|
2
2
|
import type { ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
3
3
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
4
|
import type { MainSettingsView } from '../../client-contract.ts';
|
|
5
|
+
import { type GoalRemote } from '../../picker/plan-goal.ts';
|
|
5
6
|
import { type PickerDirectoryFace } from './PickerDirectory.ts';
|
|
6
7
|
import type { PickerInteractionOperations } from './popup-dismissal.ts';
|
|
7
8
|
import { type RuntimeProviderLock } from '../runtime-lock.ts';
|
|
@@ -22,6 +23,8 @@ export interface PlanReviewFace extends PickerDirectoryFace {
|
|
|
22
23
|
refreshProviderLock: () => void;
|
|
23
24
|
subscribeMainDefaults: (listener: () => void) => () => void;
|
|
24
25
|
getMainDefaultsSnapshot: () => ConfigFormSnapshot<MainSettingsView>;
|
|
26
|
+
/** Released goal remote; absent when this deployment mounts no goal service. */
|
|
27
|
+
goalRemote: () => GoalRemote | undefined;
|
|
25
28
|
/** Live catalog-group-id → card-key map from ProviderDirectory. */
|
|
26
29
|
catalogRoutes?: () => Readonly<Record<string, string>>;
|
|
27
30
|
}
|
|
@@ -44,6 +44,10 @@ export declare const zh: {
|
|
|
44
44
|
'plan.feedbackPlaceholder': string;
|
|
45
45
|
'plan.notRestored': string;
|
|
46
46
|
'plan.dismissUnsupported': string;
|
|
47
|
+
'plan.goalRun': string;
|
|
48
|
+
'plan.goalFailed': string;
|
|
49
|
+
'plan.goalReplaced': string;
|
|
50
|
+
'plan.goalUnsupported': string;
|
|
47
51
|
'plan.responseRejected': string;
|
|
48
52
|
'plan.cancelRejected': string;
|
|
49
53
|
'markdown.copy': string;
|
|
@@ -96,6 +100,10 @@ export declare const en: {
|
|
|
96
100
|
'plan.feedbackPlaceholder': string;
|
|
97
101
|
'plan.notRestored': string;
|
|
98
102
|
'plan.dismissUnsupported': string;
|
|
103
|
+
'plan.goalRun': string;
|
|
104
|
+
'plan.goalFailed': string;
|
|
105
|
+
'plan.goalReplaced': string;
|
|
106
|
+
'plan.goalUnsupported': string;
|
|
99
107
|
'plan.responseRejected': string;
|
|
100
108
|
'plan.cancelRejected': string;
|
|
101
109
|
'markdown.copy': string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand a reviewed plan to the Host's goal domain before its review is answered.
|
|
3
|
+
*
|
|
4
|
+
* Goal state is owned by `@deepseek-ai/dsh-goal`: this module only drives the
|
|
5
|
+
* released client remote (`ctx.remote.goals`), keeps no goal state of its own,
|
|
6
|
+
* and never schedules continuation — `dsh-goal-round-driver` owns that. The
|
|
7
|
+
* shape below mirrors the published remote face structurally so the plugin
|
|
8
|
+
* neither imports generated client modules nor hard-requires the goal package
|
|
9
|
+
* in a deployment that does not mount it.
|
|
10
|
+
*
|
|
11
|
+
* Goal creation is deliberately outside DSH's own goal surface ("Goal creation
|
|
12
|
+
* remains outside this package"), so the published `remote.goals.create` is the
|
|
13
|
+
* one seam this feature relies on; if a future Host drops it, the capability
|
|
14
|
+
* reports `unsupported` instead of guessing another path.
|
|
15
|
+
*/
|
|
16
|
+
/** Compare-and-set identity of the current goal; carried by every live view. */
|
|
17
|
+
export interface GoalRef {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly revision: number;
|
|
20
|
+
}
|
|
21
|
+
/** Detached live view a client reads back from the goal remote. */
|
|
22
|
+
export interface GoalView extends GoalRef {
|
|
23
|
+
readonly phase: 'active' | 'paused' | 'blocked' | 'complete';
|
|
24
|
+
readonly activation: 'armed' | 'disarmed';
|
|
25
|
+
}
|
|
26
|
+
/** Envelope every generated remote method resolves to on the wire. */
|
|
27
|
+
export type GoalRemoteResult<T> = {
|
|
28
|
+
readonly ok: true;
|
|
29
|
+
readonly value: T;
|
|
30
|
+
} | {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
readonly error: {
|
|
33
|
+
readonly code: string;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/** The released `ctx.remote.goals` surface this feature uses. */
|
|
38
|
+
export interface GoalRemote {
|
|
39
|
+
/** Create and arm a goal for the session's live Agent. */
|
|
40
|
+
create(sessionId: string, request: {
|
|
41
|
+
readonly objective: string;
|
|
42
|
+
}): Promise<GoalRemoteResult<{
|
|
43
|
+
readonly ref: GoalRef;
|
|
44
|
+
}>>;
|
|
45
|
+
/** Current goal for the session, absent before the first create and after a clear. */
|
|
46
|
+
get(sessionId: string): Promise<GoalRemoteResult<GoalView | null | undefined>>;
|
|
47
|
+
/** Remove the current goal; its history stays in the session log. */
|
|
48
|
+
clear(sessionId: string, ref: GoalRef): Promise<GoalRemoteResult<unknown>>;
|
|
49
|
+
}
|
|
50
|
+
/** Outcome of setting a plan as the session's goal. */
|
|
51
|
+
export type GoalHandoff = {
|
|
52
|
+
readonly kind: 'created';
|
|
53
|
+
} | {
|
|
54
|
+
readonly kind: 'replaced';
|
|
55
|
+
}
|
|
56
|
+
/** The deployment mounts no goal service; the caller must not proceed. */
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: 'unsupported';
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: 'failed';
|
|
61
|
+
readonly code?: string;
|
|
62
|
+
readonly message: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Make the plan text this session's armed goal, replacing any unfinished goal.
|
|
66
|
+
*
|
|
67
|
+
* Ordering mirrors the card's own commit-then-answer rule: a refusal here keeps
|
|
68
|
+
* the Plan pending instead of approving work that no goal tracks. The replace
|
|
69
|
+
* path clears rather than edits so a new plan receives a fresh round budget —
|
|
70
|
+
* `edit` would inherit the rounds the previous goal already spent.
|
|
71
|
+
*
|
|
72
|
+
* @param remote - the released goal remote, absent when no goal package is mounted.
|
|
73
|
+
* @param sessionId - the session that owns both the review and the goal.
|
|
74
|
+
* @param plan - the plan markdown under review.
|
|
75
|
+
* @returns Whether the goal now tracks this plan, or why it does not.
|
|
76
|
+
*/
|
|
77
|
+
export declare function setPlanAsGoal(remote: GoalRemote | undefined, sessionId: string, plan: string): Promise<GoalHandoff>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-switch",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"description": "Explicit model routing plus composer/Plan picker for DeepSeek Harness",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"@deepseek-ai/dsh-compaction": ">=0.1.7-alpha.2",
|
|
120
120
|
"@deepseek-ai/dsh-compaction-basic": ">=0.1.7-alpha.2",
|
|
121
121
|
"@deepseek-ai/dsh-credentials": ">=0.1.7-alpha.2",
|
|
122
|
+
"@deepseek-ai/dsh-goal": ">=0.1.7-alpha.2",
|
|
122
123
|
"@deepseek-ai/dsh-launch-environment": ">=0.1.7-alpha.2",
|
|
123
124
|
"@deepseek-ai/dsh-llm": ">=0.1.7-alpha.2",
|
|
124
125
|
"@deepseek-ai/dsh-permission-presets": ">=0.1.7-alpha.2",
|
|
@@ -336,6 +337,9 @@
|
|
|
336
337
|
"@deepseek-ai/dsh-credentials": {
|
|
337
338
|
"optional": true
|
|
338
339
|
},
|
|
340
|
+
"@deepseek-ai/dsh-goal": {
|
|
341
|
+
"optional": true
|
|
342
|
+
},
|
|
339
343
|
"@deepseek-ai/dsh-client-connection": {
|
|
340
344
|
"optional": true
|
|
341
345
|
}
|