dsh-prompt-star 0.1.0 → 0.1.2

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,20 +4,35 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
7
  let react_jsx_runtime = require("react/jsx-runtime");
9
- //#region src/client/StarButton.tsx
8
+ let react = require("react");
9
+ //#region lib/types/client/StarButton.js
10
10
  /**
11
- * The ⭐ button rendered beside the conversation input. On click it reads the
12
- * current draft (from the session input snapshot), asks the host to generate a
13
- * fuller prompt, and writes it back with `inputActions.setDraft` — the user
14
- * presses Ctrl/Cmd+Z if they want the original draft back.
11
+ * The ⭐ button rendered beside the conversation input.
12
+ *
13
+ * Pure-client plugin: on click it reads the current draft (from the session
14
+ * input snapshot), reads a few project doc files through the already-mounted
15
+ * `workspaceFiles` remote, and assembles a fuller, more efficient prompt on the
16
+ * client, then writes it back with `inputActions.setDraft`. The user presses
17
+ * Ctrl/Cmd+Z if they want the original draft back.
15
18
  *
16
19
  * The slot framework composes this component's props: the session standard seat
17
- * (`useInput`, `inputActions`, `useConversation`) plus the register-time
18
- * `generate` face the client plugin injects. The interface below is the minimal
19
- * structural shape this component needs; it matches the composed slot props.
20
+ * (`useInput`, `inputActions`, `sessionId`) plus the register-time `workspaceFiles`
21
+ * face the client plugin injects. The interfaces below are the minimal structural
22
+ * shapes this component needs; they match the mounted `workspaceFiles` remote and
23
+ * the composed slot props without importing the harness client types.
20
24
  */
25
+ /** Common project-doc filenames probed as prompt context. */
26
+ const DOC_CANDIDATES = [
27
+ "README.md",
28
+ "README",
29
+ "AGENTS.md",
30
+ "CLAUDE.md",
31
+ "CONTRIBUTING.md",
32
+ "docs/README.md",
33
+ "docs/index.md",
34
+ ".cursorrules"
35
+ ];
21
36
  const buttonStyle = {
22
37
  all: "unset",
23
38
  display: "inline-flex",
@@ -32,7 +47,53 @@ window.__ModuleLoader__.load({
32
47
  opacity: .9,
33
48
  userSelect: "none"
34
49
  };
35
- function StarButton({ generate, useInput, inputActions, useActiveRoot }) {
50
+ /** Best-effort read of project doc files; every read is guarded so a missing
51
+ * or unreadable file is skipped rather than breaking the button. */
52
+ async function gatherContext(workspaceFiles, sessionId) {
53
+ const docs = [];
54
+ const seen = /* @__PURE__ */ new Set();
55
+ let filesRead = 0;
56
+ for (const path of DOC_CANDIDATES) try {
57
+ const result = await workspaceFiles.read(sessionId, path, {
58
+ offset: 1,
59
+ limit: 120
60
+ });
61
+ if (result.ok && result.value?.text) {
62
+ const text = result.value.text.trim();
63
+ if (text.length > 0 && !seen.has(path)) {
64
+ seen.add(path);
65
+ docs.push({
66
+ name: path,
67
+ text
68
+ });
69
+ filesRead += 1;
70
+ }
71
+ }
72
+ } catch {}
73
+ return {
74
+ docs,
75
+ filesRead
76
+ };
77
+ }
78
+ /** Assemble a fuller, structured prompt from the draft + any project context. */
79
+ function assemblePrompt(draft, context) {
80
+ return [
81
+ "请把下面的【我的草稿】整理成一段完整、清晰、高效的提示词,直接输出整理后的提示词本身。",
82
+ "",
83
+ "整理要求:",
84
+ "1. 明确任务目标、所需上下文、约束条件与期望输出形式。",
85
+ "2. 若【项目文档】提供了相关规范、术语或背景,请吸收进去,使提示词更贴合项目。",
86
+ "3. 语气中立专业,结构清晰(可用编号、小节或列表),忠于草稿原意,不虚构。",
87
+ "4. 长度以覆盖草稿要点为准,不要过度扩写。",
88
+ "",
89
+ "# 项目文档",
90
+ context.filesRead === 0 ? "(未读取到项目文档)" : context.docs.map((doc) => `## ${doc.name}\n${doc.text}`).join("\n\n"),
91
+ "",
92
+ "# 我的草稿",
93
+ draft
94
+ ].join("\n");
95
+ }
96
+ function StarButton({ workspaceFiles, useInput, inputActions, sessionId }) {
36
97
  const input = useInput();
37
98
  const [busy, setBusy] = (0, react.useState)(false);
38
99
  const [error, setError] = (0, react.useState)(void 0);
@@ -45,49 +106,67 @@ window.__ModuleLoader__.load({
45
106
  setBusy(true);
46
107
  setError(void 0);
47
108
  try {
48
- const result = await generate({
49
- draft,
50
- workspaceRoot: useActiveRoot?.(),
51
- intent: void 0
52
- });
53
- if (result.prompt && result.prompt !== draft) {
54
- inputActions.setDraft(result.prompt);
55
- if (result.degraded) setError("模型调用失败,已保留原草稿");
56
- } else setError(result.degraded ? "模型调用失败,未生成新内容" : "生成结果与原草稿相同,未替换");
109
+ const context = await gatherContext(workspaceFiles, sessionId);
110
+ const full = assemblePrompt(draft, context);
111
+ if (full && full !== draft) {
112
+ inputActions.setDraft(full);
113
+ setError(context.filesRead === 0 ? "已按草稿整理(未读取到项目文档)" : `已整理并读取 ${context.filesRead} 个项目文档`);
114
+ } else setError("未生成新内容");
57
115
  } catch (cause) {
58
- const message = cause instanceof Error ? cause.message : String(cause);
59
- setError(`生成失败: ${message}`);
116
+ setError(`生成失败: ${cause instanceof Error ? cause.message : String(cause)}`);
60
117
  } finally {
61
118
  setBusy(false);
62
119
  }
63
120
  }
64
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
121
+ return (0, react_jsx_runtime.jsx)("button", {
65
122
  type: "button",
66
123
  style: buttonStyle,
67
- title: error ?? "prompt-star: 生成更完整的提示词",
124
+ title: error ?? "prompt-star: 整理成更完整的提示词",
68
125
  disabled: busy,
69
126
  onClick,
70
127
  children: busy ? "…" : "⭐"
71
128
  });
72
129
  }
73
130
  //#endregion
74
- //#region src/client/index.ts
75
- /** Cordis services this client plugin needs. */
76
- const inject = ["slots", "remote"];
131
+ //#region lib/types/client/index.js
132
+ /**
133
+ * Client plugin: mounts the ⭐ button into the composer input tool row
134
+ * (`conversation.input.right`, a session-scoped list slot) and wires it to the
135
+ * already-mounted `workspaceFiles` remote.
136
+ *
137
+ * This is a pure-client plugin: it reads the draft via the session input
138
+ * snapshot, reads project doc files through `ctx.remote.workspaceFiles`, and
139
+ * assembles a fuller prompt on the client — no custom host↔client RPC is needed,
140
+ * so it works in any stock `dsh web` build.
141
+ */
142
+ /** Cordis services this client plugin needs: the slot registry and the mounted
143
+ * `workspaceFiles` remote (both provided by the stock web-app composition). */
144
+ const inject = [
145
+ "slots",
146
+ "remote",
147
+ "remote.workspaceFiles"
148
+ ];
77
149
  /**
78
150
  * Client plugin body: register the ⭐ button once the slot registry and the
79
- * remote face are up.
151
+ * `workspaceFiles` remote are up.
80
152
  * @param ctx - client root context.
81
153
  */
82
154
  function apply(ctx) {
83
- ctx.inject(["slots", "remote"], (scope) => {
84
- const promptStar = scope.remote.promptStar;
85
- scope.slots.inject("conversation.input.right", () => scope.slots.register({
86
- name: "conversation.input.right",
87
- id: "prompt-star",
88
- order: 0,
89
- inject: () => ({ generate: promptStar.generate })
90
- }, StarButton));
155
+ ctx.inject([
156
+ "slots",
157
+ "remote",
158
+ "remote.workspaceFiles"
159
+ ], (scope) => {
160
+ const root = scope;
161
+ const { workspaceFiles } = root.remote;
162
+ root.slots.inject("conversation.input.right", () => {
163
+ root.slots.register({
164
+ name: "conversation.input.right",
165
+ id: "prompt-star",
166
+ order: 0,
167
+ inject: () => ({ workspaceFiles })
168
+ }, StarButton);
169
+ });
91
170
  });
92
171
  }
93
172
  //#endregion
package/lib/index.js CHANGED
@@ -1,283 +1,21 @@
1
- import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
- import { readFile } from "node:fs/promises";
3
- import { isAbsolute, join } from "node:path";
4
- import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { deadline } from "@deepseek-ai/dsh-timeout";
6
- import { deepFreeze } from "@deepseek-ai/dsh-util-values";
7
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/usingCtx.js
8
- function _usingCtx() {
9
- var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
10
- var n = Error();
11
- return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
12
- }, e = {}, n = [];
13
- function using(r, e) {
14
- if (null != e) {
15
- if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
16
- if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
17
- if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
18
- if ("function" != typeof o) throw new TypeError("Object is not disposable.");
19
- t && (o = function o() {
20
- try {
21
- t.call(e);
22
- } catch (r) {
23
- return Promise.reject(r);
24
- }
25
- }), n.push({
26
- v: e,
27
- d: o,
28
- a: r
29
- });
30
- } else r && n.push({
31
- d: e,
32
- a: r
33
- });
34
- return e;
35
- }
36
- return {
37
- e,
38
- u: using.bind(null, !1),
39
- a: using.bind(null, !0),
40
- d: function d() {
41
- var o, t = this.e, s = 0;
42
- function next() {
43
- for (; o = n.pop();) try {
44
- if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
45
- if (o.d) {
46
- var r = o.d.call(o.v);
47
- if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
48
- } else s |= 1;
49
- } catch (r) {
50
- return err(r);
51
- }
52
- if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
53
- if (t !== e) throw t;
54
- }
55
- function err(n) {
56
- return t = t !== e ? new r(n, t) : n, next();
57
- }
58
- return next();
59
- }
60
- };
61
- }
62
- //#endregion
63
- //#region src/host/generate.ts
64
- /**
65
- * Host implementation of one prompt-star generate: probe the project's doc
66
- * files for context, then call the configured default LLM to turn the draft +
67
- * context into a fuller prompt. Returns a best-effort result (never throws) so
68
- * a model failure degrades to a copy rather than failing the click.
69
- */
70
- /** Doc files we probe (first match by priority wins as main context; all read). */
71
- const DOC_FILE_NAMES = [
72
- "CLAUDE.md",
73
- "AGENTS.md",
74
- ".cursorrules",
75
- "README.md"
76
- ];
77
- /** Per-file read cap; keeps a huge README from eating the model input budget. */
78
- const MAX_DOC_BYTES = 8e3;
79
- /** Output-token cap for the generated prompt. */
80
- const MAX_OUTPUT_TOKENS = 1200;
81
- /** End-to-end generate deadline. */
82
- const TIMEOUT_MS = 2e4;
83
- /** Timeout code registered for the capability. */
84
- const PROMPT_STAR_TIMEOUT_CODE = "PROMPT_STAR_TIMEOUT";
1
+ //#region lib/types/index.js
85
2
  /**
86
- * Model-visible purpose. `dsh-llm` types this as the union
87
- * `'compaction' | 'session-title'`; this plugin's own purpose requires a
88
- * one-line addition to that union (see README "Patch dsh-llm"). The cast keeps
89
- * this package compiling before the patch lands.
3
+ * Host entry for dsh-prompt-star.
4
+ *
5
+ * The ⭐ button is a pure client plugin (see `src/client/`): it reads the
6
+ * conversation draft via the session input snapshot, reads project doc files
7
+ * through the already-mounted `workspaceFiles` remote, and assembles a fuller,
8
+ * more efficient prompt entirely on the client. It needs no custom host↔client
9
+ * RPC, so this host half only provides a minimal Cordis plugin that keeps the
10
+ * bundle mounted as a Loader entry — which is what makes `dsh-client-modules`
11
+ * discover and serve its client bundle.
12
+ *
13
+ * This entry follows the standard DSH plugin export contract `apply(ctx)`.
90
14
  */
91
- const PURPOSE = "prompt-star";
92
- /** Inline template skeletons the model reuses as a shape, not a strict form. */
93
- const SKELETONS = [
94
- {
95
- id: "request",
96
- title: "提需求",
97
- hint: "目标 / 用户 / 验收标准 / 边界 / 依赖"
98
- },
99
- {
100
- id: "bug",
101
- title: "改 bug",
102
- hint: "现象 / 复现步骤 / 期望 vs 实际 / 触发条件 / 环境"
103
- },
104
- {
105
- id: "letter",
106
- title: "写信",
107
- hint: "称呼 / 目的 / 语气 / 落款"
108
- },
109
- {
110
- id: "generic",
111
- title: "通用",
112
- hint: "目标 / 输入 / 期望输出 / 约束"
113
- }
114
- ];
115
- /** Default workspace root used when the caller cannot supply one. */
116
- function defaultRoot(ctx) {
117
- return process.cwd();
118
- }
119
- /** Cap a path at the workspace root (defence-in-depth: refuses abs paths outside root). */
120
- function keepInside(root, path) {
121
- if (isAbsolute(path)) return path.startsWith(root) ? path : void 0;
122
- return join(root, path);
123
- }
124
- /** Probe the doc files, reading the first `MAX_DOC_BYTES` bytes of each hit. */
125
- async function readDocFiles(root) {
126
- const sources = [];
127
- for (const name of DOC_FILE_NAMES) {
128
- const path = keepInside(root, name);
129
- if (path === void 0) continue;
130
- try {
131
- const buf = await readFile(path);
132
- sources.push({
133
- path,
134
- name,
135
- read: true,
136
- bytes: Math.min(buf.byteLength, MAX_DOC_BYTES)
137
- });
138
- } catch {
139
- sources.push({
140
- path,
141
- name,
142
- read: false,
143
- bytes: 0
144
- });
145
- }
146
- }
147
- return sources;
148
- }
149
- /** Frame the doc context into the system prompt (references, not whole files). */
150
- function frameContext(sources) {
151
- const hits = sources.filter((source) => source.read);
152
- if (hits.length === 0) return "当前项目没有可读的说明文件(未找到 CLAUDE.md / AGENTS.md / .cursorrules / README.md)。";
153
- return hits.map((source) => `--- ${source.name} (${source.bytes} 字节) ---\n<probe-only: use this file as project context>`).join("\n");
154
- }
155
- /** Compose the system instruction that turns a draft into a fuller prompt. */
156
- function buildSystem(sources) {
157
- return [
158
- "你是提示词优化器。用户会给一段随手写的草稿,你把它扩写成一版更完整、更省 token、意图清晰的提示词。",
159
- "阅读下面用到的项目说明文件(若有)作为上下文,不要照抄,只借用其术语、技术栈与约定。",
160
- "可参考的模板骨架(选择与该草稿最贴切的一个作为形状,按需补充):",
161
- SKELETONS.map((skeleton) => `- ${skeleton.title}(${skeleton.id}):${skeleton.hint}`).join("\n"),
162
- "输出要求:",
163
- "- 只输出优化后的提示词正文,不要解释、不要 Markdown 包裹、不要前缀。",
164
- "- 保留用户草稿的原意,补全缺失的上下文与验收标准。",
165
- "- 用草稿同语言输出。",
166
- "",
167
- frameContext(sources)
168
- ].join("\n");
169
- }
170
- /** Map the LLM finish reason to an error, mirroring session-title-llm. */
171
- function finishError(finish) {
172
- switch (finish.kind) {
173
- case "stop": return;
174
- case "error":
175
- case "aborted": {
176
- const error = new Error(finish.failure.message);
177
- error.code = finish.failure.code;
178
- return error;
179
- }
180
- case "max-tokens": return /* @__PURE__ */ new Error("prompt-star: output reached maxTokens");
181
- case "tool-calls": return /* @__PURE__ */ new Error("prompt-star: model unexpectedly requested a tool");
182
- default: return /* @__PURE__ */ new Error(`prompt-star: unsupported finish reason "${String(finish.kind)}"`);
183
- }
184
- }
185
- /** Read the plain text of one assembled LLM reply. */
186
- function blocksToText(blocks) {
187
- return blocks.filter((block) => block.type === "text").map((block) => block.text).join(" ");
188
- }
189
15
  /**
190
- * Generate the optimized prompt.
191
- * @param ctx - Host context carrying the `llm` service.
192
- * @param request - draft + optional workspace root / intent.
193
- * @returns a settled result; degraded=true when the model call failed.
16
+ * Minimal host plugin body. The button logic lives entirely in the client
17
+ * bundle; mounting this row is what lets the web app serve `lib/client.js`.
194
18
  */
195
- async function generatePrompt(ctx, request) {
196
- const sources = await readDocFiles(request.workspaceRoot ?? defaultRoot(ctx));
197
- const system = buildSystem(sources);
198
- const userText = request.intent?.trim() ? `草稿:\n${request.draft}\n\n请侧重「${request.intent}」模板。` : `草稿:\n${request.draft}`;
199
- try {
200
- try {
201
- var _usingCtx$1 = _usingCtx();
202
- const messages = [createUserMessage({
203
- content: [{
204
- type: "text",
205
- text: userText
206
- }],
207
- source: {
208
- kind: "plugin",
209
- plugin: "dsh-prompt-star"
210
- }
211
- })];
212
- const callDeadline = _usingCtx$1.u(deadline(request.signal ?? new AbortController().signal, TIMEOUT_MS, PROMPT_STAR_TIMEOUT_CODE));
213
- const options = deepFreeze({
214
- provider: void 0,
215
- model: void 0,
216
- messages,
217
- system,
218
- maxTokens: MAX_OUTPUT_TOKENS,
219
- purpose: PURPOSE,
220
- signal: callDeadline.signal
221
- });
222
- callDeadline.signal.throwIfAborted();
223
- const assembler = new BlockAssembler();
224
- for await (const chunk of ctx.llm.stream(options)) {
225
- callDeadline.signal.throwIfAborted();
226
- assembler.push(chunk);
227
- }
228
- callDeadline.signal.throwIfAborted();
229
- const terminalError = finishError(assembler.finish);
230
- if (terminalError !== void 0) throw terminalError;
231
- const blocks = assembler.blocks();
232
- if (blocks.some((block) => block.type === "tool-call")) throw new Error("prompt-star: output must contain text only");
233
- const prompt = blocksToText(blocks).trim();
234
- if (prompt.length === 0) throw new Error("prompt-star: model produced no text");
235
- return {
236
- prompt,
237
- intent: request.intent ?? inferIntent(prompt, system),
238
- sources,
239
- model: void 0,
240
- degraded: false
241
- };
242
- } catch (_) {
243
- _usingCtx$1.e = _;
244
- } finally {
245
- _usingCtx$1.d();
246
- }
247
- } catch (error) {
248
- return {
249
- prompt: request.draft,
250
- intent: request.intent ?? "generic",
251
- sources,
252
- model: void 0,
253
- degraded: true
254
- };
255
- }
256
- }
257
- /** Cheap intent label: prefer the supplied one, else the first skeleton hit. */
258
- function inferIntent(prompt, _system) {
259
- prompt.toLowerCase();
260
- return SKELETONS.find((skeleton) => skeleton.id === "request" || skeleton.id === "bug" || skeleton.id === "letter")?.id ?? "generic";
261
- }
262
- //#endregion
263
- //#region src/index.ts
264
- /** Host service backing the generated `ctx.remote.promptStar` namespace. */
265
- var PromptStarService = class extends TypertRemoteService {
266
- static inject = ["typert"];
267
- ctx;
268
- /** @param ctx - Host context exposing the LLM and workspace services. */
269
- constructor(ctx) {
270
- super(ctx, "promptStar", { namespace: "promptStar" });
271
- this.ctx = ctx;
272
- }
273
- /**
274
- * Turn a draft + project context into a fuller prompt.
275
- * @param request - draft and optional workspace root / intent / signal.
276
- * @returns generated prompt and the doc files probed for context.
277
- */
278
- @Remote("generate") async generate(request) {
279
- return generatePrompt(this.ctx, request);
280
- }
281
- };
19
+ function apply(_ctx) {}
282
20
  //#endregion
283
- export { PromptStarService, PromptStarService as default };
21
+ export { apply, apply as default };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The ⭐ button rendered beside the conversation input.
3
+ *
4
+ * Pure-client plugin: on click it reads the current draft (from the session
5
+ * input snapshot), reads a few project doc files through the already-mounted
6
+ * `workspaceFiles` remote, and assembles a fuller, more efficient prompt on the
7
+ * client, then writes it back with `inputActions.setDraft`. The user presses
8
+ * Ctrl/Cmd+Z if they want the original draft back.
9
+ *
10
+ * The slot framework composes this component's props: the session standard seat
11
+ * (`useInput`, `inputActions`, `sessionId`) plus the register-time `workspaceFiles`
12
+ * face the client plugin injects. The interfaces below are the minimal structural
13
+ * shapes this component needs; they match the mounted `workspaceFiles` remote and
14
+ * the composed slot props without importing the harness client types.
15
+ */
16
+ import { type ReactElement } from 'react';
17
+ /** Minimal structural face of the mounted `workspaceFiles` remote. */
18
+ export interface WorkspaceFilesRemote {
19
+ read(sessionId: string, path: string, range: {
20
+ offset?: number;
21
+ limit?: number;
22
+ }, signal?: AbortSignal): Promise<{
23
+ ok: boolean;
24
+ value?: {
25
+ text: string;
26
+ eof?: boolean;
27
+ };
28
+ }>;
29
+ list(sessionId: string, path: string, signal?: AbortSignal): Promise<{
30
+ ok: boolean;
31
+ value?: {
32
+ entries: Array<{
33
+ name: string;
34
+ type: string;
35
+ }>;
36
+ };
37
+ }>;
38
+ }
39
+ export interface StarButtonProps {
40
+ /** Mounted `workspaceFiles` remote (reads project docs). */
41
+ workspaceFiles: WorkspaceFilesRemote;
42
+ /** Session input snapshot hook (returns the current draft). */
43
+ useInput(): {
44
+ draft: string;
45
+ };
46
+ /** Session public input actions (write the whole draft). */
47
+ inputActions: {
48
+ setDraft(text: string): void;
49
+ };
50
+ /** Current session identity, used as the wire identity for file reads. */
51
+ sessionId: string;
52
+ }
53
+ export declare function StarButton({ workspaceFiles, useInput, inputActions, sessionId, }: StarButtonProps): ReactElement;
54
+ //# sourceMappingURL=StarButton.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The ⭐ button rendered beside the conversation input.
4
+ *
5
+ * Pure-client plugin: on click it reads the current draft (from the session
6
+ * input snapshot), reads a few project doc files through the already-mounted
7
+ * `workspaceFiles` remote, and assembles a fuller, more efficient prompt on the
8
+ * client, then writes it back with `inputActions.setDraft`. The user presses
9
+ * Ctrl/Cmd+Z if they want the original draft back.
10
+ *
11
+ * The slot framework composes this component's props: the session standard seat
12
+ * (`useInput`, `inputActions`, `sessionId`) plus the register-time `workspaceFiles`
13
+ * face the client plugin injects. The interfaces below are the minimal structural
14
+ * shapes this component needs; they match the mounted `workspaceFiles` remote and
15
+ * the composed slot props without importing the harness client types.
16
+ */
17
+ import { useState } from 'react';
18
+ /** Common project-doc filenames probed as prompt context. */
19
+ const DOC_CANDIDATES = [
20
+ 'README.md',
21
+ 'README',
22
+ 'AGENTS.md',
23
+ 'CLAUDE.md',
24
+ 'CONTRIBUTING.md',
25
+ 'docs/README.md',
26
+ 'docs/index.md',
27
+ '.cursorrules',
28
+ ];
29
+ const buttonStyle = {
30
+ all: 'unset',
31
+ display: 'inline-flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'center',
34
+ width: '30px',
35
+ height: '30px',
36
+ cursor: 'pointer',
37
+ borderRadius: '6px',
38
+ fontSize: '16px',
39
+ lineHeight: 1,
40
+ opacity: 0.9,
41
+ userSelect: 'none',
42
+ };
43
+ /** Best-effort read of project doc files; every read is guarded so a missing
44
+ * or unreadable file is skipped rather than breaking the button. */
45
+ async function gatherContext(workspaceFiles, sessionId) {
46
+ const docs = [];
47
+ const seen = new Set();
48
+ let filesRead = 0;
49
+ for (const path of DOC_CANDIDATES) {
50
+ try {
51
+ const result = await workspaceFiles.read(sessionId, path, { offset: 1, limit: 120 });
52
+ if (result.ok && result.value?.text) {
53
+ const text = result.value.text.trim();
54
+ if (text.length > 0 && !seen.has(path)) {
55
+ seen.add(path);
56
+ docs.push({ name: path, text });
57
+ filesRead += 1;
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // Not present / not readable: leave it out.
63
+ }
64
+ }
65
+ return { docs, filesRead };
66
+ }
67
+ /** Assemble a fuller, structured prompt from the draft + any project context. */
68
+ function assemblePrompt(draft, context) {
69
+ const projectContext = context.filesRead === 0
70
+ ? '(未读取到项目文档)'
71
+ : context.docs.map((doc) => `## ${doc.name}\n${doc.text}`).join('\n\n');
72
+ return [
73
+ '请把下面的【我的草稿】整理成一段完整、清晰、高效的提示词,直接输出整理后的提示词本身。',
74
+ '',
75
+ '整理要求:',
76
+ '1. 明确任务目标、所需上下文、约束条件与期望输出形式。',
77
+ '2. 若【项目文档】提供了相关规范、术语或背景,请吸收进去,使提示词更贴合项目。',
78
+ '3. 语气中立专业,结构清晰(可用编号、小节或列表),忠于草稿原意,不虚构。',
79
+ '4. 长度以覆盖草稿要点为准,不要过度扩写。',
80
+ '',
81
+ '# 项目文档',
82
+ projectContext,
83
+ '',
84
+ '# 我的草稿',
85
+ draft,
86
+ ].join('\n');
87
+ }
88
+ export function StarButton({ workspaceFiles, useInput, inputActions, sessionId, }) {
89
+ const input = useInput();
90
+ const [busy, setBusy] = useState(false);
91
+ const [error, setError] = useState(undefined);
92
+ async function onClick() {
93
+ const draft = input.draft.trim();
94
+ if (draft.length === 0) {
95
+ setError('草稿为空,先写点什么再点 ⭐');
96
+ return;
97
+ }
98
+ setBusy(true);
99
+ setError(undefined);
100
+ try {
101
+ const context = await gatherContext(workspaceFiles, sessionId);
102
+ const full = assemblePrompt(draft, context);
103
+ if (full && full !== draft) {
104
+ inputActions.setDraft(full);
105
+ setError(context.filesRead === 0
106
+ ? '已按草稿整理(未读取到项目文档)'
107
+ : `已整理并读取 ${context.filesRead} 个项目文档`);
108
+ }
109
+ else {
110
+ setError('未生成新内容');
111
+ }
112
+ }
113
+ catch (cause) {
114
+ const message = cause instanceof Error ? cause.message : String(cause);
115
+ setError(`生成失败: ${message}`);
116
+ }
117
+ finally {
118
+ setBusy(false);
119
+ }
120
+ }
121
+ return (_jsx("button", { type: "button", style: buttonStyle, title: error ?? 'prompt-star: 整理成更完整的提示词', disabled: busy, onClick: onClick, children: busy ? '…' : '⭐' }));
122
+ }
123
+ //# sourceMappingURL=StarButton.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Client plugin: mounts the ⭐ button into the composer input tool row
3
+ * (`conversation.input.right`, a session-scoped list slot) and wires it to the
4
+ * already-mounted `workspaceFiles` remote.
5
+ *
6
+ * This is a pure-client plugin: it reads the draft via the session input
7
+ * snapshot, reads project doc files through `ctx.remote.workspaceFiles`, and
8
+ * assembles a fuller prompt on the client — no custom host↔client RPC is needed,
9
+ * so it works in any stock `dsh web` build.
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ /** Cordis services this client plugin needs: the slot registry and the mounted
13
+ * `workspaceFiles` remote (both provided by the stock web-app composition). */
14
+ export declare const inject: string[];
15
+ /**
16
+ * Client plugin body: register the ⭐ button once the slot registry and the
17
+ * `workspaceFiles` remote are up.
18
+ * @param ctx - client root context.
19
+ */
20
+ export declare function apply(ctx: Context): void;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Client plugin: mounts the ⭐ button into the composer input tool row
3
+ * (`conversation.input.right`, a session-scoped list slot) and wires it to the
4
+ * already-mounted `workspaceFiles` remote.
5
+ *
6
+ * This is a pure-client plugin: it reads the draft via the session input
7
+ * snapshot, reads project doc files through `ctx.remote.workspaceFiles`, and
8
+ * assembles a fuller prompt on the client — no custom host↔client RPC is needed,
9
+ * so it works in any stock `dsh web` build.
10
+ */
11
+ import { StarButton } from "./StarButton.js";
12
+ /** Cordis services this client plugin needs: the slot registry and the mounted
13
+ * `workspaceFiles` remote (both provided by the stock web-app composition). */
14
+ export const inject = ['slots', 'remote', 'remote.workspaceFiles'];
15
+ /**
16
+ * Client plugin body: register the ⭐ button once the slot registry and the
17
+ * `workspaceFiles` remote are up.
18
+ * @param ctx - client root context.
19
+ */
20
+ export function apply(ctx) {
21
+ ctx.inject(['slots', 'remote', 'remote.workspaceFiles'], (scope) => {
22
+ const root = scope;
23
+ const { workspaceFiles } = root.remote;
24
+ root.slots.inject('conversation.input.right', () => {
25
+ root.slots.register({
26
+ name: 'conversation.input.right',
27
+ id: 'prompt-star',
28
+ order: 0,
29
+ inject: () => ({ workspaceFiles }),
30
+ }, StarButton);
31
+ });
32
+ });
33
+ }
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Host entry for dsh-prompt-star.
3
+ *
4
+ * The ⭐ button is a pure client plugin (see `src/client/`): it reads the
5
+ * conversation draft via the session input snapshot, reads project doc files
6
+ * through the already-mounted `workspaceFiles` remote, and assembles a fuller,
7
+ * more efficient prompt entirely on the client. It needs no custom host↔client
8
+ * RPC, so this host half only provides a minimal Cordis plugin that keeps the
9
+ * bundle mounted as a Loader entry — which is what makes `dsh-client-modules`
10
+ * discover and serve its client bundle.
11
+ *
12
+ * This entry follows the standard DSH plugin export contract `apply(ctx)`.
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ /**
16
+ * Minimal host plugin body. The button logic lives entirely in the client
17
+ * bundle; mounting this row is what lets the web app serve `lib/client.js`.
18
+ */
19
+ export declare function apply(_ctx: Context): void;
20
+ export default apply;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Host entry for dsh-prompt-star.
3
+ *
4
+ * The ⭐ button is a pure client plugin (see `src/client/`): it reads the
5
+ * conversation draft via the session input snapshot, reads project doc files
6
+ * through the already-mounted `workspaceFiles` remote, and assembles a fuller,
7
+ * more efficient prompt entirely on the client. It needs no custom host↔client
8
+ * RPC, so this host half only provides a minimal Cordis plugin that keeps the
9
+ * bundle mounted as a Loader entry — which is what makes `dsh-client-modules`
10
+ * discover and serve its client bundle.
11
+ *
12
+ * This entry follows the standard DSH plugin export contract `apply(ctx)`.
13
+ */
14
+ /**
15
+ * Minimal host plugin body. The button logic lives entirely in the client
16
+ * bundle; mounting this row is what lets the web app serve `lib/client.js`.
17
+ */
18
+ export function apply(_ctx) {
19
+ // no-op by design: the client plugin does the real work.
20
+ }
21
+ export default apply;
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared request/result types for the prompt-star host Remote namespace.
3
+ * Kept free of Cordis/Node so the client bundle can import them.
4
+ */
5
+ /** One project doc file the host probes and may read as context. */
6
+ export interface DocFileSource {
7
+ /** Absolute path as resolved on the host. */
8
+ readonly path: string;
9
+ /** Basename (CLAUDE.md / AGENTS.md / .cursorrules / README.md). */
10
+ readonly name: string;
11
+ /** Whether the file existed and was read. */
12
+ readonly read: boolean;
13
+ /** Bytes read, capped; 0 when `read` is false. */
14
+ readonly bytes: number;
15
+ }
16
+ /** Client → Host: a click of ⭐ with the current draft. */
17
+ export interface PromptStarRequest {
18
+ /** Current draft text (the editor's clipboard-text projection). */
19
+ readonly draft: string;
20
+ /**
21
+ * Session workspace root when the client knows it, else undefined and the
22
+ * host falls back to its own resolution / process.cwd().
23
+ */
24
+ readonly workspaceRoot?: string;
25
+ /** Pick a template intent explicitly; empty lets the model infer it. */
26
+ readonly intent?: string;
27
+ /** Optional caller cancellation; the host falls back to an internal deadline. */
28
+ readonly signal?: AbortSignal;
29
+ }
30
+ /** Host → Client: the generated prompt ready to fill the input. */
31
+ export interface PromptStarResult {
32
+ /** Generated full prompt text. */
33
+ readonly prompt: string;
34
+ /** Intent/template the model chose (or the supplied `intent`). */
35
+ readonly intent: string;
36
+ /** Doc files the host probed for context. */
37
+ readonly sources: readonly DocFileSource[];
38
+ /** Model route actually used (provider/model when available). */
39
+ readonly model?: {
40
+ readonly provider: string;
41
+ readonly model: string;
42
+ };
43
+ /** True when a transient/LLM failure was swallowed and `prompt` is a best-effort copy. */
44
+ readonly degraded: boolean;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared request/result types for the prompt-star host Remote namespace.
3
+ * Kept free of Cordis/Node so the client bundle can import them.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-prompt-star",
3
- "version": "0.1.0",
4
- "description": "DSH plugin: a ⭐ button beside the conversation input that reads your draft + the project's doc files and generates a fuller, more efficient prompt, filling the input directly (Ctrl/Cmd+Z to undo).",
3
+ "version": "0.1.2",
4
+ "description": "DSH plugin: a ⭐ button beside the conversation input that reads your draft + the project's doc files and turns them into a fuller, more efficient prompt, filling the input directly (Ctrl/Cmd+Z to undo).",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -59,11 +59,13 @@
59
59
  "devDependencies": {
60
60
  "@types/react": "~18.3.1",
61
61
  "react": "^18.3.1",
62
- "tsdown": "^0.14.0",
62
+ "tsdown": "^0.22.2",
63
63
  "typescript": "^5.5.0"
64
64
  },
65
65
  "scripts": {
66
66
  "bundle": "tsdown",
67
+ "bundle:host": "tsdown --env.DSH_BUILD_FACE host",
68
+ "bundle:client": "tsdown --env.DSH_BUILD_FACE client",
67
69
  "watch": "tsdown --watch"
68
70
  },
69
71
  "license": "MIT"