@norman-else/dsh-claude 0.1.41 → 0.1.42

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/lib/client.js CHANGED
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  module.exports;
6
6
  var { Fragment, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } = require("react");
7
- var { DiffBlock, DisclosureRow, HoverCard, IconAgentPresetOutline16, IconApiOutline14, IconBranchOutline16, IconCheckOutline14, IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconChevronUpOutline14, IconCloseOutline16, IconEditOutline16, IconEllipsisOutline16, IconFullscreenOutline16, IconQueueOutline14, IconRefreshOutline14, IconRightUpOutline14, IconSearchOutline16, IconSendOutline14, IconThinkOutline14, IconTrashOutline16, MarkdownText, Menu, Modal, StateDot, Toast, Tooltip, useDismissOnOutsidePointer } = require("@deepseek-ai/dsh-client-ui-primitives");
7
+ var { Button, DiffBlock, DisclosureRow, HoverCard, IconAgentPresetOutline16, IconApiOutline14, IconBranchOutline16, IconCheckOutline14, IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconChevronUpOutline14, IconCloseOutline16, IconEditOutline16, IconEllipsisOutline16, IconFullscreenOutline16, IconListPenOutline16, IconLoadingOutline16, IconQueueOutline14, IconRefreshOutline14, IconRefreshOutline16, IconRightUpOutline14, IconSearchOutline16, IconSendOutline14, IconSparkle16, IconThinkOutline14, IconTrashOutline16, MarkdownText, Menu, Modal, StateDot, Toast, Tooltip, useDismissOnOutsidePointer } = require("@deepseek-ai/dsh-client-ui-primitives");
8
8
  var { Fragment: Fragment$1, jsx, jsxs } = require("react/jsx-runtime");
9
9
  var { createPortal } = require("react-dom");
10
10
  /** Claude's subagent dispatch tools; rendered as plugin-owned group cards
@@ -28,6 +28,9 @@ window.__ModuleLoader__.load({
28
28
  const CLAUDE_EDITOR_OPEN_PATH = "/plugins/dsh-claude/editor/open";
29
29
  const CLAUDE_REWIND_PATH = "/plugins/dsh-claude/rewind";
30
30
  const CLAUDE_PLAN_FEEDBACK_PATH = "/plugins/dsh-claude/plan/feedback";
31
+ const CLAUDE_PROMPTS_PATH = "/plugins/dsh-claude/prompts";
32
+ const CLAUDE_PROMPT_NAME_PATH = "/plugins/dsh-claude/prompts/name";
33
+ const CLAUDE_PROMPT_REFINE_PATH = "/plugins/dsh-claude/prompts/refine";
31
34
  function isClaudeRenderMode(value) {
32
35
  return value === "plugin" || value === "native";
33
36
  }
@@ -1834,7 +1837,7 @@ window.__ModuleLoader__.load({
1834
1837
  whiteSpace: "nowrap"
1835
1838
  };
1836
1839
  const repositoryBarFrame = {
1837
- width: "calc(100% - 64px)",
1840
+ width: "calc(100% - 2 * var(--dsh-composer-side-clearance, 16px))",
1838
1841
  maxWidth: "var(--dsh-composer-card-max-width, 782px)",
1839
1842
  minWidth: 0,
1840
1843
  margin: "0 auto",
@@ -2994,7 +2997,7 @@ window.__ModuleLoader__.load({
2994
2997
  fontSize: 11
2995
2998
  };
2996
2999
  const reviewCommentBarFrame = {
2997
- width: "calc(100% - 64px)",
3000
+ width: "calc(100% - 2 * var(--dsh-composer-side-clearance, 16px))",
2998
3001
  maxWidth: "var(--dsh-composer-card-max-width, 782px)",
2999
3002
  margin: "0 auto",
3000
3003
  boxSizing: "border-box"
@@ -3085,6 +3088,113 @@ window.__ModuleLoader__.load({
3085
3088
  whiteSpace: "pre-wrap",
3086
3089
  overflowWrap: "anywhere"
3087
3090
  };
3091
+ const promptSaveTriggerClass = "dshClaudePromptSaveTrigger";
3092
+ const promptSpinClass = "dshClaudePromptSpin";
3093
+ const promptUndoClass = "dshClaudePromptUndo";
3094
+ const promptSaveFieldClass = "dshClaudePromptSaveField";
3095
+ /** The composer's own attach button geometry (InputBar.module.css `.add`:
3096
+ * 28x28 circle on --dsw-specific-selector, 14px glyph). The primitives'
3097
+ * Button has no icon-only form — its `sm` is a 36px-wide capsule around a
3098
+ * 16px slot, which is why this reads oversized beside its neighbours. */
3099
+ const promptSaveTriggerCss = `
3100
+ .${promptSaveTriggerClass} {
3101
+ width: 28px;
3102
+ height: 28px;
3103
+ flex: none;
3104
+ display: grid;
3105
+ place-items: center;
3106
+ padding: 0;
3107
+ border: none;
3108
+ border-radius: 999px;
3109
+ background: var(--dsw-specific-selector);
3110
+ color: var(--dsw-alias-label-primary);
3111
+ cursor: pointer;
3112
+ transition: background-color 120ms ease, color 120ms ease;
3113
+ }
3114
+ /* With no draft there is nothing to keep, so the control drops its seat
3115
+ * entirely rather than sitting there as a filled-but-dead circle. */
3116
+ .${promptSaveTriggerClass}:disabled {
3117
+ cursor: default;
3118
+ background: transparent;
3119
+ color: var(--dsw-alias-label-tertiary);
3120
+ }
3121
+ .${promptSaveTriggerClass}:focus-visible { outline: none; }
3122
+ .${promptSaveFieldClass} {
3123
+ box-sizing: border-box;
3124
+ width: 100%;
3125
+ height: 30px;
3126
+ padding: 0 9px;
3127
+ border: 1px solid var(--dsw-alias-border-inverted);
3128
+ border-radius: 7px;
3129
+ background: var(--dsw-alias-interactive-bg-hover);
3130
+ color: var(--dsw-alias-label-primary);
3131
+ font-family: var(--dsw-font-family);
3132
+ font-size: 13px;
3133
+ line-height: 20px;
3134
+ }
3135
+ /* Not --dsw-alias-brand-primary: in the dark theme that alias resolves to
3136
+ * neutral-bluish-50, so a focused field draws a near-white slab around
3137
+ * itself. The focus ring is a step up in the same greys the card is made of. */
3138
+ .${promptSaveFieldClass}:focus-visible {
3139
+ outline: none;
3140
+ border-color: var(--dsw-alias-label-tertiary);
3141
+ background: var(--dsw-alias-interactive-bg-active);
3142
+ }
3143
+ .${promptSaveFieldClass}:disabled { opacity: 0.6; }
3144
+ /* ic_ds_loading_outline_16 is a bare open ring and nothing in the primitives
3145
+ * turns it, so a four-second rewrite sat behind a frozen glyph that reads as
3146
+ * the letter C. The spin is what makes it a spinner. */
3147
+ .${promptSpinClass} { animation: dshClaudePromptSpin 900ms linear infinite; }
3148
+ @keyframes dshClaudePromptSpin { to { transform: rotate(360deg); } }
3149
+ /* The icon set has no undo arrow, and ic_ds_refresh_outline_16 turns
3150
+ * clockwise — the redo direction. Mirrored, it is the ordinary undo glyph. */
3151
+ .${promptUndoClass} { transform: scaleX(-1); }
3152
+ @media (prefers-reduced-motion: reduce) {
3153
+ .${promptSpinClass} { animation: none; }
3154
+ }
3155
+ `;
3156
+ /** The naming card hangs off the composer tool row, portaled so the composer
3157
+ * card cannot clip it and positioned by useAnchoredPosition. */
3158
+ const promptSaveCard = {
3159
+ position: "fixed",
3160
+ zIndex: 120,
3161
+ boxSizing: "border-box",
3162
+ display: "flex",
3163
+ flexDirection: "column",
3164
+ gap: 8,
3165
+ width: 340,
3166
+ maxWidth: "calc(100vw - 32px)",
3167
+ padding: "10px 12px",
3168
+ border: "1px solid var(--dsw-alias-border-inverted)",
3169
+ borderRadius: 11,
3170
+ background: "var(--dsw-specific-menu)",
3171
+ boxShadow: "var(--dsw-shadow-lv3)",
3172
+ color: "var(--dsw-alias-label-primary)",
3173
+ fontSize: 12,
3174
+ lineHeight: "20px"
3175
+ };
3176
+ const promptSaveHeading = {
3177
+ color: "var(--dsw-alias-label-tertiary)",
3178
+ fontSize: 11,
3179
+ lineHeight: "16px"
3180
+ };
3181
+ const promptSaveActions = {
3182
+ display: "flex",
3183
+ justifyContent: "flex-end",
3184
+ gap: 6
3185
+ };
3186
+ /** The saved file, wrapped rather than truncated: a path the user cannot read
3187
+ * in full does not tell them where to go and edit it. */
3188
+ const promptSaveLocation = {
3189
+ color: "var(--dsw-alias-label-tertiary)",
3190
+ fontSize: 11,
3191
+ lineHeight: "16px",
3192
+ overflowWrap: "anywhere"
3193
+ };
3194
+ const promptSaveError = {
3195
+ color: "var(--dsw-static-red-450, #d64545)",
3196
+ overflowWrap: "anywhere"
3197
+ };
3088
3198
  const diffAhead = { color: "var(--dsw-static-blue-450)" };
3089
3199
  const diffAheadMuted = { color: "color-mix(in srgb, var(--dsw-static-blue-450) 62%, var(--dsw-alias-label-tertiary))" };
3090
3200
  const diffAddMuted = { color: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 62%, var(--dsw-alias-label-tertiary))" };
@@ -4008,6 +4118,16 @@ window.__ModuleLoader__.load({
4008
4118
  laneHeld[lane] -= 1;
4009
4119
  pump();
4010
4120
  }
4121
+ /** Runs at most once, so a release can be wired to every ending of a stream
4122
+ * without any of them having to know about the others. */
4123
+ function once(action) {
4124
+ let done = false;
4125
+ return () => {
4126
+ if (done) return;
4127
+ done = true;
4128
+ action();
4129
+ };
4130
+ }
4011
4131
  function acquire(lane) {
4012
4132
  let released = false;
4013
4133
  const releaseOnce = () => {
@@ -4111,11 +4231,53 @@ window.__ModuleLoader__.load({
4111
4231
  writeId += 1;
4112
4232
  return writeId;
4113
4233
  }
4234
+ /**
4235
+ * A reader that gives its permit back however the stream ends.
4236
+ *
4237
+ * The permit used to be released by one event only -- the caller's abort --
4238
+ * and a stream has three endings, two of which the caller does not cause: the
4239
+ * server closing the response, and the body failing mid-read. Either one left
4240
+ * the permit held. For the reserved carrier that permit is a single boolean,
4241
+ * so one such ending closed the transcript stream for the life of the page and
4242
+ * every reopen answered `starved`, silently, forever.
4243
+ *
4244
+ * Binding the release to the reader rather than to the signal is what makes
4245
+ * that unrepresentable: a caller cannot hold the permit past the stream it is
4246
+ * reading, because every way of leaving the read passes through here. `free`
4247
+ * is idempotent, so the abort listener below stays as the belt to this brace.
4248
+ */
4249
+ function releasingReader(reader, free) {
4250
+ return {
4251
+ get closed() {
4252
+ return reader.closed;
4253
+ },
4254
+ read: async () => {
4255
+ try {
4256
+ const chunk = await reader.read();
4257
+ if (chunk.done) free();
4258
+ return chunk;
4259
+ } catch (error) {
4260
+ free();
4261
+ throw error;
4262
+ }
4263
+ },
4264
+ cancel: async (reason) => {
4265
+ try {
4266
+ await reader.cancel(reason);
4267
+ } finally {
4268
+ free();
4269
+ }
4270
+ },
4271
+ releaseLock: () => {
4272
+ reader.releaseLock();
4273
+ }
4274
+ };
4275
+ }
4114
4276
  async function openStream(lane, path, cancel, options, reserved) {
4115
4277
  const url = withQuery(path, options?.query);
4116
- const free = reserved ? () => {
4278
+ const free = reserved ? once(() => {
4117
4279
  projectionHeld = false;
4118
- } : await acquire(lane);
4280
+ }) : await acquire(lane);
4119
4281
  try {
4120
4282
  const response = await send(url, {
4121
4283
  method: options?.method ?? "GET",
@@ -4133,7 +4295,7 @@ window.__ModuleLoader__.load({
4133
4295
  throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
4134
4296
  }
4135
4297
  cancel.addEventListener("abort", free, { once: true });
4136
- return response.body.getReader();
4298
+ return releasingReader(response.body.getReader(), free);
4137
4299
  } catch (error) {
4138
4300
  free();
4139
4301
  throw failureOf(error, cancel);
@@ -4598,6 +4760,10 @@ window.__ModuleLoader__.load({
4598
4760
  #report;
4599
4761
  #resyncCooldownMs;
4600
4762
  #resyncedAt = 0;
4763
+ /** Whether the carrier is in a run of failures. One report per outage: the
4764
+ * retry loop runs every couple of seconds and a beacon per attempt would
4765
+ * be the plugin reporting its own noise. */
4766
+ #carrierFailing = false;
4601
4767
  #controller;
4602
4768
  #settle;
4603
4769
  #running = false;
@@ -4683,6 +4849,7 @@ window.__ModuleLoader__.load({
4683
4849
  let superseded = false;
4684
4850
  try {
4685
4851
  const reader = await this.#open(`${CLAUDE_PROJECTION_PATH}/multi?sessions=${lanes.map(encodeURIComponent).join(",")}`, controller.signal);
4852
+ this.#carrierFailing = false;
4686
4853
  const stop = () => {
4687
4854
  superseded = true;
4688
4855
  reader.cancel().catch(() => void 0);
@@ -4705,6 +4872,10 @@ window.__ModuleLoader__.load({
4705
4872
  if (this.#controller !== controller) continue;
4706
4873
  return;
4707
4874
  }
4875
+ if (!this.#carrierFailing) {
4876
+ this.#carrierFailing = true;
4877
+ this.#report("projection-carrier-unavailable", error instanceof Error ? error.message : String(error));
4878
+ }
4708
4879
  } finally {
4709
4880
  if (this.#controller === controller) this.#controller = void 0;
4710
4881
  }
@@ -17642,6 +17813,428 @@ window.__ModuleLoader__.load({
17642
17813
  }
17643
17814
  }
17644
17815
  //#endregion
17816
+ //#region src/client/prompt-api.ts
17817
+ /** Prompt files live in one global directory, so one module-level cache serves
17818
+ * every session. The TTL is what lets a file added outside DSH appear without
17819
+ * a reload, while a burst of keystrokes through the menu still costs one
17820
+ * directory scan. */
17821
+ const TTL_MS = 5e3;
17822
+ let cached;
17823
+ function invalidateClaudePrompts() {
17824
+ cached = void 0;
17825
+ }
17826
+ /** The user's prompt snippets, at most one read per TTL window. */
17827
+ async function claudePrompts() {
17828
+ const at = Date.now();
17829
+ if (cached !== void 0 && at - cached.at < TTL_MS) return await cached.prompts;
17830
+ const prompts = pluginRead(CLAUDE_PROMPTS_PATH, "fast").then((payload) => payload.prompts).catch(() => {
17831
+ invalidateClaudePrompts();
17832
+ return [];
17833
+ });
17834
+ cached = {
17835
+ at,
17836
+ prompts
17837
+ };
17838
+ return await prompts;
17839
+ }
17840
+ /** Save one snippet, answering where it landed. Rejects with a
17841
+ * `PluginRequestError` whose `code` is `name-taken` when the file already
17842
+ * exists; nothing is overwritten. */
17843
+ async function saveClaudePrompt(name, body) {
17844
+ const saved = await pluginWrite(CLAUDE_PROMPTS_PATH, "fast", void 0, { json: {
17845
+ name,
17846
+ body
17847
+ } });
17848
+ invalidateClaudePrompts();
17849
+ return saved.prompt;
17850
+ }
17851
+ /** Ask the host to name a draft with Claude's cheapest model. Answers
17852
+ * undefined whenever no name could be had — the caller already holds the
17853
+ * locally derived one, so a failure here is never worth reporting. */
17854
+ async function suggestClaudePromptName(draft, cancel) {
17855
+ try {
17856
+ return (await pluginWrite(CLAUDE_PROMPT_NAME_PATH, "git", cancel, { json: { draft } })).name;
17857
+ } catch {
17858
+ return;
17859
+ }
17860
+ }
17861
+ /** Rewrite a draft into something an agent can act on. Unlike the name
17862
+ * suggestion this one throws: the user asked for it and is waiting, so a
17863
+ * failure is theirs to see rather than ours to swallow. */
17864
+ async function refineClaudePrompt(draft, cancel) {
17865
+ return (await pluginWrite(CLAUDE_PROMPT_REFINE_PATH, "git", cancel, { json: { draft } })).text;
17866
+ }
17867
+ /** Characters the host's `PROMPT_NAME` guard rejects; scrubbed rather than
17868
+ * re-implemented here, so a drift in the guard cannot let a bad name through
17869
+ * (the host still validates, and answers `invalid-name`). */
17870
+ const FOREIGN_NAME_CHARS = /[^\p{L}\p{M}\p{N} ._()\[\]-]/gu;
17871
+ const MAX_NAME_CHARS = 40;
17872
+ function stamp(now) {
17873
+ const pad = (value) => String(value).padStart(2, "0");
17874
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
17875
+ }
17876
+ /** The file name to offer for a draft: its opening line, scrubbed to what a
17877
+ * file name may hold. A draft that opens with punctuation or an emoji leaves
17878
+ * nothing usable, so those fall back to a timestamp the user can rename. */
17879
+ function defaultPromptName(draft, now = /* @__PURE__ */ new Date()) {
17880
+ const collapsed = (draft.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "").replace(FOREIGN_NAME_CHARS, " ").replace(/\s+/gu, " ").trim();
17881
+ const cut = collapsed.slice(0, MAX_NAME_CHARS);
17882
+ const boundary = collapsed.length > MAX_NAME_CHARS ? cut.search(/[\s\-_][^\s\-_]*$/u) : -1;
17883
+ const scrubbed = (boundary > 0 ? cut.slice(0, boundary) : cut).trim();
17884
+ return /^[\p{L}\p{N}]/u.test(scrubbed) ? scrubbed : `prompt-${stamp(now)}`;
17885
+ }
17886
+ //#endregion
17887
+ //#region src/client/ClaudePromptSaveAction.tsx
17888
+ const CARD_GAP = 8;
17889
+ const CARD_MARGIN = 12;
17890
+ /**
17891
+ * Place the card above its trigger, clamped into the viewport, and dismiss it
17892
+ * on a pointer that lands outside both.
17893
+ *
17894
+ * The primitives ship `useAnchoredPosition` and `useDismissOnOutsidePointer`
17895
+ * for exactly this, and both are used here through their published types —
17896
+ * but the Host's build of that package predates the `side` option of one and
17897
+ * the portal argument of the other. Neither omission is visible to `tsc`,
17898
+ * which reads this checkout's newer copy, and both are silent at runtime: the
17899
+ * card hangs below a composer pinned to the bottom of the window, and every
17900
+ * pointer landing in the naming field counts as outside and closes it. Owning
17901
+ * twenty lines beats behaviour that depends on which Desktop the plugin was
17902
+ * installed into.
17903
+ */
17904
+ function useAnchoredCard(open, anchor, card, onDismiss) {
17905
+ const [position, setPosition] = useState();
17906
+ useLayoutEffect(() => {
17907
+ if (!open) {
17908
+ setPosition(void 0);
17909
+ return;
17910
+ }
17911
+ const place = () => {
17912
+ const trigger = anchor.current?.getBoundingClientRect();
17913
+ const panel = card.current?.getBoundingClientRect();
17914
+ if (trigger === void 0 || panel === void 0) return;
17915
+ const above = trigger.top - CARD_GAP - panel.height;
17916
+ setPosition({
17917
+ left: Math.max(CARD_MARGIN, Math.min(trigger.left, window.innerWidth - panel.width - CARD_MARGIN)),
17918
+ top: above >= CARD_MARGIN ? above : Math.min(trigger.bottom + CARD_GAP, window.innerHeight - panel.height - CARD_MARGIN)
17919
+ });
17920
+ };
17921
+ place();
17922
+ window.addEventListener("resize", place);
17923
+ window.addEventListener("scroll", place, true);
17924
+ return () => {
17925
+ window.removeEventListener("resize", place);
17926
+ window.removeEventListener("scroll", place, true);
17927
+ };
17928
+ }, [
17929
+ open,
17930
+ anchor,
17931
+ card
17932
+ ]);
17933
+ useEffect(() => {
17934
+ if (!open) return void 0;
17935
+ const dismiss = (event) => {
17936
+ const target = event.target;
17937
+ if (!(target instanceof Node)) return;
17938
+ if (anchor.current?.contains(target) === true || card.current?.contains(target) === true) return;
17939
+ onDismiss();
17940
+ };
17941
+ document.addEventListener("pointerdown", dismiss);
17942
+ return () => {
17943
+ document.removeEventListener("pointerdown", dismiss);
17944
+ };
17945
+ }, [
17946
+ open,
17947
+ anchor,
17948
+ card,
17949
+ onDismiss
17950
+ ]);
17951
+ return position;
17952
+ }
17953
+ /**
17954
+ * Keep the draft you just wrote, from the composer's own tool row.
17955
+ *
17956
+ * It sits beside the attach and access controls rather than in a row of its
17957
+ * own: a band above the composer moves the repository bar and the composer
17958
+ * itself every time a draft appears, and this is a once-in-a-while action that
17959
+ * has not earned that. The naming card is portaled and anchored, so opening it
17960
+ * displaces nothing either.
17961
+ *
17962
+ * The field opens on a name derived from the draft's first line, which costs
17963
+ * nothing and is there instantly, and a Claude-written name replaces it when
17964
+ * one arrives. That ordering is the whole naming design: the suggestion is an
17965
+ * improvement on a working answer, never something the user waits for.
17966
+ */
17967
+ function ClaudePromptSaveAction({ t, useClaudeProjection, input, savePrompt = saveClaudePrompt, suggestName = suggestClaudePromptName }) {
17968
+ const owned = useClaudeProjection((projection) => projection.owned);
17969
+ const anchor = useRef(null);
17970
+ const panelRef = useRef(null);
17971
+ const suggestion = useRef(void 0);
17972
+ const [panel, setPanel] = useState();
17973
+ const [saving, setSaving] = useState(false);
17974
+ const close = useCallback(() => {
17975
+ suggestion.current?.abort();
17976
+ suggestion.current = void 0;
17977
+ setPanel(void 0);
17978
+ }, []);
17979
+ const position = useAnchoredCard(panel !== void 0, anchor, panelRef, close);
17980
+ useEffect(() => () => {
17981
+ suggestion.current?.abort();
17982
+ }, []);
17983
+ if (!owned) return null;
17984
+ const draft = input?.draft ?? "";
17985
+ const label = t("promptSave");
17986
+ const open = () => {
17987
+ setPanel({
17988
+ kind: "naming",
17989
+ name: defaultPromptName(draft),
17990
+ touched: false,
17991
+ suggesting: true
17992
+ });
17993
+ const attempt = new AbortController();
17994
+ suggestion.current = attempt;
17995
+ suggestName(draft, attempt.signal).then((suggested) => {
17996
+ if (attempt.signal.aborted) return;
17997
+ setPanel((current) => current?.kind !== "naming" ? current : {
17998
+ ...current,
17999
+ suggesting: false,
18000
+ ...suggested === void 0 || current.touched ? {} : { name: suggested }
18001
+ });
18002
+ });
18003
+ };
18004
+ const save = () => {
18005
+ const name = panel?.kind === "naming" ? panel.name.trim() : "";
18006
+ if (saving || name === "" || draft.trim() === "") return;
18007
+ setSaving(true);
18008
+ savePrompt(name, draft).then((prompt) => {
18009
+ setPanel({
18010
+ kind: "saved",
18011
+ prompt
18012
+ });
18013
+ }, (error) => {
18014
+ setPanel({
18015
+ kind: "naming",
18016
+ name,
18017
+ touched: true,
18018
+ suggesting: false,
18019
+ failure: error instanceof PluginRequestError && error.code === "name-taken" ? t("promptSaveExists") : t("promptSaveFailed", { message: error instanceof Error ? error.message : String(error) })
18020
+ });
18021
+ }).finally(() => {
18022
+ setSaving(false);
18023
+ });
18024
+ };
18025
+ return /* @__PURE__ */ jsxs("span", {
18026
+ ref: anchor,
18027
+ style: {
18028
+ position: "relative",
18029
+ display: "inline-flex"
18030
+ },
18031
+ children: [
18032
+ /* @__PURE__ */ jsx("style", {
18033
+ "data-dsh-claude-prompt-save-styles": true,
18034
+ children: promptSaveTriggerCss
18035
+ }),
18036
+ /* @__PURE__ */ jsx(Tooltip, {
18037
+ label,
18038
+ side: "top",
18039
+ delayMs: 250,
18040
+ disabled: panel !== void 0,
18041
+ children: /* @__PURE__ */ jsx("button", {
18042
+ type: "button",
18043
+ className: promptSaveTriggerClass,
18044
+ "aria-label": label,
18045
+ "aria-haspopup": "dialog",
18046
+ "aria-expanded": panel !== void 0,
18047
+ disabled: draft.trim() === "",
18048
+ onClick: () => {
18049
+ if (panel === void 0) open();
18050
+ else close();
18051
+ },
18052
+ children: /* @__PURE__ */ jsx(IconListPenOutline16, { size: 14 })
18053
+ })
18054
+ }),
18055
+ panel === void 0 || typeof document === "undefined" ? null : createPortal(/* @__PURE__ */ jsx("div", {
18056
+ ref: panelRef,
18057
+ style: {
18058
+ ...promptSaveCard,
18059
+ ...position
18060
+ },
18061
+ role: "dialog",
18062
+ "aria-label": label,
18063
+ children: panel.kind === "saved" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
18064
+ /* @__PURE__ */ jsx("span", { children: t("promptSaved", { name: panel.prompt.name }) }),
18065
+ /* @__PURE__ */ jsx("span", {
18066
+ style: promptSaveLocation,
18067
+ children: panel.prompt.location
18068
+ }),
18069
+ /* @__PURE__ */ jsx("span", {
18070
+ style: promptSaveActions,
18071
+ children: /* @__PURE__ */ jsx(Button, {
18072
+ variant: "primary",
18073
+ size: "sm",
18074
+ onClick: close,
18075
+ children: t("promptSaveDone")
18076
+ })
18077
+ })
18078
+ ] }) : /* @__PURE__ */ jsxs("form", {
18079
+ style: { display: "contents" },
18080
+ onSubmit: (event) => {
18081
+ event.preventDefault();
18082
+ save();
18083
+ },
18084
+ children: [
18085
+ /* @__PURE__ */ jsx("span", {
18086
+ style: promptSaveHeading,
18087
+ children: panel.suggesting ? t("promptSaveNaming") : label
18088
+ }),
18089
+ /* @__PURE__ */ jsx("input", {
18090
+ className: promptSaveFieldClass,
18091
+ "aria-label": t("promptSaveName"),
18092
+ placeholder: t("promptSaveName"),
18093
+ value: panel.name,
18094
+ maxLength: 128,
18095
+ autoFocus: true,
18096
+ disabled: saving,
18097
+ onChange: (event) => setPanel({
18098
+ ...panel,
18099
+ name: event.currentTarget.value,
18100
+ touched: true
18101
+ }),
18102
+ onKeyDown: (event) => {
18103
+ if (event.key === "Escape") close();
18104
+ }
18105
+ }),
18106
+ panel.failure === void 0 ? null : /* @__PURE__ */ jsx("span", {
18107
+ style: promptSaveError,
18108
+ children: panel.failure
18109
+ }),
18110
+ /* @__PURE__ */ jsxs("span", {
18111
+ style: promptSaveActions,
18112
+ children: [/* @__PURE__ */ jsx(Button, {
18113
+ variant: "ghost",
18114
+ size: "sm",
18115
+ type: "button",
18116
+ disabled: saving,
18117
+ onClick: close,
18118
+ children: t("promptSaveCancel")
18119
+ }), /* @__PURE__ */ jsx(Button, {
18120
+ variant: "primary",
18121
+ size: "sm",
18122
+ type: "submit",
18123
+ disabled: saving,
18124
+ children: t("promptSaveConfirm")
18125
+ })]
18126
+ })
18127
+ ]
18128
+ })
18129
+ }), document.body)
18130
+ ]
18131
+ });
18132
+ }
18133
+ //#endregion
18134
+ //#region src/client/ClaudePromptRefineAction.tsx
18135
+ /**
18136
+ * Rewrite the draft in place, from the composer's own tool row.
18137
+ *
18138
+ * The rewrite replaces the draft outright, which is what makes the undo state
18139
+ * load-bearing rather than a nicety: `SessionInput.setDraft` documents itself
18140
+ * as "merged into history so a seed is not an undoable step of its own", so
18141
+ * Ctrl/Cmd+Z does NOT bring the original back. Without somewhere to put it,
18142
+ * one press of this button would silently destroy a draft the user may have
18143
+ * spent minutes on. So the original is held for exactly as long as it is still
18144
+ * recoverable — until the rewrite is edited or sent — and the same button
18145
+ * offers it back over that window.
18146
+ */
18147
+ function ClaudePromptRefineAction({ t, useClaudeProjection, input, replaceDraft, notify, refine = refineClaudePrompt }) {
18148
+ const owned = useClaudeProjection((projection) => projection.owned);
18149
+ const attempt = useRef(void 0);
18150
+ const [busy, setBusy] = useState(false);
18151
+ const [applied, setApplied] = useState();
18152
+ useEffect(() => () => {
18153
+ attempt.current?.abort();
18154
+ }, []);
18155
+ if (!owned || replaceDraft === void 0) return null;
18156
+ const draft = input?.draft ?? "";
18157
+ const undoable = applied !== void 0 && draft === applied.refined;
18158
+ const label = busy ? t("promptRefineBusy") : undoable ? t("promptRefineUndo") : t("promptRefine");
18159
+ const run = () => {
18160
+ if (busy || draft.trim() === "") return;
18161
+ setBusy(true);
18162
+ const running = new AbortController();
18163
+ attempt.current = running;
18164
+ refine(draft, running.signal).then((text) => {
18165
+ if (running.signal.aborted) return;
18166
+ setApplied({
18167
+ original: draft,
18168
+ refined: text
18169
+ });
18170
+ replaceDraft(text);
18171
+ }, (error) => {
18172
+ if (running.signal.aborted) return;
18173
+ notify?.("error", t("promptRefineFailed", { message: error instanceof Error ? error.message : String(error) }));
18174
+ }).finally(() => {
18175
+ setBusy(false);
18176
+ });
18177
+ };
18178
+ const undo = () => {
18179
+ if (applied === void 0) return;
18180
+ replaceDraft(applied.original);
18181
+ setApplied(void 0);
18182
+ };
18183
+ return /* @__PURE__ */ jsx(Tooltip, {
18184
+ label,
18185
+ side: "top",
18186
+ delayMs: 250,
18187
+ children: /* @__PURE__ */ jsxs("button", {
18188
+ type: "button",
18189
+ className: promptSaveTriggerClass,
18190
+ "aria-label": label,
18191
+ disabled: busy || !undoable && draft.trim() === "",
18192
+ onClick: undoable ? undo : run,
18193
+ children: [/* @__PURE__ */ jsx("style", {
18194
+ "data-dsh-claude-prompt-refine-styles": true,
18195
+ children: promptSaveTriggerCss
18196
+ }), busy ? /* @__PURE__ */ jsx(IconLoadingOutline16, {
18197
+ size: 14,
18198
+ className: promptSpinClass
18199
+ }) : undoable ? /* @__PURE__ */ jsx(IconRefreshOutline16, {
18200
+ size: 14,
18201
+ className: promptUndoClass
18202
+ }) : /* @__PURE__ */ jsx(IconSparkle16, { size: 14 })]
18203
+ })
18204
+ });
18205
+ }
18206
+ //#endregion
18207
+ //#region src/client/claude-prompt-source.ts
18208
+ /**
18209
+ * The user's own prompt snippets as a second `/` group.
18210
+ *
18211
+ * Unlike {@link createClaudeCommandSource}, a pick here settles as plain text:
18212
+ * the pipeline replaces the trigger token with the snippet body and leaves the
18213
+ * caret after it, so the draft stays editable and nothing is sent. A snippet is
18214
+ * a half-written message, not a command.
18215
+ */
18216
+ function createClaudePromptSource(groupName, load = claudePrompts) {
18217
+ let known = [];
18218
+ return {
18219
+ trigger: "/",
18220
+ name: groupName,
18221
+ order: 20,
18222
+ async candidates(_session, request) {
18223
+ if (request.position !== "leading") return [];
18224
+ known = await load();
18225
+ const query = request.query.toLocaleLowerCase();
18226
+ return known.filter((prompt) => prompt.name.toLocaleLowerCase().includes(query)).map((prompt) => ({
18227
+ name: prompt.name,
18228
+ description: prompt.description
18229
+ }));
18230
+ },
18231
+ onPick(pick) {
18232
+ const prompt = known.find((item) => item.name === pick.candidate.name);
18233
+ return prompt === void 0 ? void 0 : { text: prompt.body };
18234
+ }
18235
+ };
18236
+ }
18237
+ //#endregion
17645
18238
  //#region src/client/preset-seat-mark.ts
17646
18239
  /** Flags the Host's agent-preset seat while it names the Claude preset.
17647
18240
  *
@@ -17940,7 +18533,7 @@ window.__ModuleLoader__.load({
17940
18533
  worktreeBranchPrefixEffect: "分支前缀修改会应用于之后自动创建的 Worktree 分支;显式指定的完整分支名不会被修改。",
17941
18534
  maxProcessesSetting: "Claude 进程上限",
17942
18535
  idleTimeoutSetting: "闲置回收时长(分钟)",
17943
- maxProcessesEffect: "进程上限在下次启动 Claude 进程时生效。留空或非法值会被拒绝。",
18536
+ maxProcessesEffect: "保存后立即回收超额空闲进程;运行中的进程完成后再收敛。满载时,新会话会等待可用容量。留空或非法值会被拒绝。",
17944
18537
  idleTimeoutEffect: "闲置回收时长在下一次回合结束后生效。留空或非法值会被拒绝。",
17945
18538
  settingHint: "查看说明",
17946
18539
  pluginUpdate: "插件更新",
@@ -18098,6 +18691,20 @@ window.__ModuleLoader__.load({
18098
18691
  sessionMenu: "会话菜单",
18099
18692
  sessionMenuOpenIn: "打开方式",
18100
18693
  sessionMenuOpenFailed: "无法打开:{message}",
18694
+ promptSource: "常用提示词",
18695
+ promptSave: "存为常用提示词",
18696
+ promptSaveName: "名字",
18697
+ promptSaveNaming: "正在起名…",
18698
+ promptRefine: "AI 优化提示词",
18699
+ promptRefineBusy: "优化中…",
18700
+ promptRefineUndo: "还原成优化前的内容",
18701
+ promptRefineFailed: "优化失败:{message}",
18702
+ promptSaveConfirm: "保存",
18703
+ promptSaveCancel: "取消",
18704
+ promptSaveDone: "知道了",
18705
+ promptSaved: "已存为「{name}」",
18706
+ promptSaveExists: "已有同名提示词,换个名字。",
18707
+ promptSaveFailed: "保存失败:{message}",
18101
18708
  presetHeaderHint: "当前会话运行的 Agent 预设",
18102
18709
  diffWorkingTree: "分支改动",
18103
18710
  diffFiles: "{count} 个已修改文件",
@@ -18350,7 +18957,7 @@ window.__ModuleLoader__.load({
18350
18957
  worktreeBranchPrefixEffect: "Prefix changes apply to subsequently generated Worktree branches. Explicit full branch names remain unchanged.",
18351
18958
  maxProcessesSetting: "Claude process limit",
18352
18959
  idleTimeoutSetting: "Idle timeout (minutes)",
18353
- maxProcessesEffect: "The process limit applies when the next Claude process is admitted. Blank or invalid values are rejected.",
18960
+ maxProcessesEffect: "Saving immediately reclaims excess idle processes; running processes converge after they finish. New sessions wait for capacity while every process is busy. Blank or invalid values are rejected.",
18354
18961
  idleTimeoutEffect: "The idle timeout applies after the next completed turn. Blank or invalid values are rejected.",
18355
18962
  settingHint: "Show details",
18356
18963
  pluginUpdate: "Plugin updates",
@@ -18508,6 +19115,20 @@ window.__ModuleLoader__.load({
18508
19115
  sessionMenu: "Session menu",
18509
19116
  sessionMenuOpenIn: "Open in",
18510
19117
  sessionMenuOpenFailed: "Could not open: {message}",
19118
+ promptSource: "Prompts",
19119
+ promptSave: "Save as a prompt",
19120
+ promptSaveName: "Name",
19121
+ promptSaveNaming: "Naming it…",
19122
+ promptRefine: "Rewrite with AI",
19123
+ promptRefineBusy: "Rewriting…",
19124
+ promptRefineUndo: "Put the original back",
19125
+ promptRefineFailed: "Could not rewrite: {message}",
19126
+ promptSaveConfirm: "Save",
19127
+ promptSaveCancel: "Cancel",
19128
+ promptSaveDone: "Got it",
19129
+ promptSaved: "Saved as \"{name}\"",
19130
+ promptSaveExists: "A prompt with that name already exists. Try another.",
19131
+ promptSaveFailed: "Could not save: {message}",
18511
19132
  presetHeaderHint: "The agent preset this session runs",
18512
19133
  diffWorkingTree: "Branch changes",
18513
19134
  diffFiles: "{count} modified file(s)",
@@ -18785,6 +19406,7 @@ window.__ModuleLoader__.load({
18785
19406
  diagnostics.report(kind, detail);
18786
19407
  } });
18787
19408
  ctx.effect(() => ctx.inputTriggers.registerSource(createClaudeCommandSource(ctx, projections)), "dsh-claude: Claude slash source");
19409
+ ctx.effect(() => ctx.inputTriggers.registerSource(createClaudePromptSource(t("promptSource"))), "dsh-claude: Claude prompt source");
18788
19410
  const sessions = ctx.get("sessions");
18789
19411
  const workspaces = ctx.get("workspaces");
18790
19412
  const uiWorkspace = ctx.get("uiWorkspace");
@@ -19131,6 +19753,34 @@ window.__ModuleLoader__.load({
19131
19753
  locale: namespace,
19132
19754
  inject: () => ({ t })
19133
19755
  }, ClaudeSessionMenu));
19756
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
19757
+ name: "conversation.input.left",
19758
+ id: "claude-prompt-save",
19759
+ order: 40,
19760
+ locale: namespace,
19761
+ inject: () => ({ t })
19762
+ }, ClaudePromptSaveAction));
19763
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
19764
+ name: "conversation.input.left",
19765
+ id: "claude-prompt-refine",
19766
+ order: 41,
19767
+ locale: namespace,
19768
+ inject: (sessionId) => {
19769
+ if (sessions === void 0 || conversation === void 0) return { t };
19770
+ const scope = sessions.scope(sessionId);
19771
+ if (scope === void 0) return { t };
19772
+ const facade = sessionInput(conversation, scope);
19773
+ return {
19774
+ t,
19775
+ replaceDraft: (text) => {
19776
+ facade.setDraft(text);
19777
+ },
19778
+ notify: (level, text) => {
19779
+ facade.notify(level, text);
19780
+ }
19781
+ };
19782
+ }
19783
+ }, ClaudePromptRefineAction));
19134
19784
  ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
19135
19785
  name: "conversation.input.dock",
19136
19786
  id: "claude-review-comments",