@treeseed/dsh-chapters 0.2.0 → 0.2.2-dev.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/cordis.patch.yml +7 -0
- package/docs/backlog.md +1566 -0
- package/docs/contract.md +281 -0
- package/docs/development.md +122 -0
- package/docs/host-protected-head-ask.md +80 -0
- package/docs/project-state.md +266 -0
- package/docs/verify.md +61 -0
- package/lib/arrival.js +24 -0
- package/lib/arrival.js.map +1 -1
- package/lib/auto-save.d.ts +1 -1
- package/lib/auto-save.js +15 -2
- package/lib/auto-save.js.map +1 -1
- package/lib/aux-message.d.ts +17 -0
- package/lib/aux-message.js +20 -0
- package/lib/aux-message.js.map +1 -0
- package/lib/client.js +536 -227
- package/lib/client.js.map +1 -1
- package/lib/commands.js +107 -1
- package/lib/commands.js.map +1 -1
- package/lib/continue-core.d.ts +6 -0
- package/lib/continue-core.js +18 -13
- package/lib/continue-core.js.map +1 -1
- package/lib/diag.d.ts +16 -8
- package/lib/diag.js +46 -1
- package/lib/diag.js.map +1 -1
- package/lib/engine-core.d.ts +5 -0
- package/lib/engine-core.js +5 -3
- package/lib/engine-core.js.map +1 -1
- package/lib/engine.d.ts +15 -0
- package/lib/engine.js +79 -6
- package/lib/engine.js.map +1 -1
- package/lib/index.d.ts +6 -0
- package/lib/index.js +202 -9
- package/lib/index.js.map +1 -1
- package/lib/injections.js +10 -0
- package/lib/injections.js.map +1 -1
- package/lib/prompt-loop.d.ts +73 -0
- package/lib/prompt-loop.js +141 -0
- package/lib/prompt-loop.js.map +1 -0
- package/lib/prompt-runner.d.ts +44 -0
- package/lib/prompt-runner.js +85 -0
- package/lib/prompt-runner.js.map +1 -0
- package/lib/prompt-state.d.ts +124 -0
- package/lib/prompt-state.js +96 -0
- package/lib/prompt-state.js.map +1 -0
- package/lib/prompt.d.ts +32 -2
- package/lib/prompt.js +31 -9
- package/lib/prompt.js.map +1 -1
- package/lib/registry.d.ts +7 -0
- package/lib/registry.js +2 -0
- package/lib/registry.js.map +1 -1
- package/lib/render.d.ts +22 -1
- package/lib/render.js +40 -7
- package/lib/render.js.map +1 -1
- package/lib/repair-run.d.ts +27 -0
- package/lib/repair-run.js +85 -2
- package/lib/repair-run.js.map +1 -1
- package/lib/rules.js +8 -0
- package/lib/rules.js.map +1 -1
- package/lib/save-core.js +4 -4
- package/lib/save-core.js.map +1 -1
- package/lib/store.js +11 -0
- package/lib/store.js.map +1 -1
- package/lib/tools.d.ts +3 -0
- package/lib/tools.js +29 -6
- package/lib/tools.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +20 -16
package/lib/client.js
CHANGED
|
@@ -6,7 +6,524 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
-
|
|
9
|
+
// src/client/prompt-bar.ts
|
|
10
|
+
function initialBarState(autorun = "disarmed") {
|
|
11
|
+
return {
|
|
12
|
+
status: "off",
|
|
13
|
+
text: null,
|
|
14
|
+
turnSeq: null,
|
|
15
|
+
error: null,
|
|
16
|
+
note: "",
|
|
17
|
+
intent: "worker-step",
|
|
18
|
+
goalPhase: "none",
|
|
19
|
+
goal: null,
|
|
20
|
+
autorun,
|
|
21
|
+
expanded: false,
|
|
22
|
+
running: false,
|
|
23
|
+
hiddenTurnSeq: null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function promptBarReducer(state, ev) {
|
|
27
|
+
if (ev.t === "note") return {
|
|
28
|
+
...state,
|
|
29
|
+
note: ev.note
|
|
30
|
+
};
|
|
31
|
+
const next = baseBarReducer(state, ev);
|
|
32
|
+
return ev.t === "polled" && (next.turnSeq !== state.turnSeq || next.text !== state.text) ? {
|
|
33
|
+
...next,
|
|
34
|
+
note: ""
|
|
35
|
+
} : next;
|
|
36
|
+
}
|
|
37
|
+
function baseBarReducer(state, ev) {
|
|
38
|
+
switch (ev.t) {
|
|
39
|
+
case "polled":
|
|
40
|
+
if (ev.status === "off") return {
|
|
41
|
+
...state,
|
|
42
|
+
status: "off",
|
|
43
|
+
text: null,
|
|
44
|
+
turnSeq: null,
|
|
45
|
+
error: null,
|
|
46
|
+
expanded: false,
|
|
47
|
+
autorun: "disarmed"
|
|
48
|
+
};
|
|
49
|
+
if (ev.status === "analyzing") return {
|
|
50
|
+
...state,
|
|
51
|
+
status: "analyzing",
|
|
52
|
+
text: null,
|
|
53
|
+
error: null
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
...state,
|
|
57
|
+
status: ev.status,
|
|
58
|
+
text: ev.status === "ready" ? ev.text ?? null : null,
|
|
59
|
+
intent: ev.status === "ready" ? ev.intent ?? "worker-step" : "worker-step",
|
|
60
|
+
turnSeq: ev.turnSeq ?? state.turnSeq,
|
|
61
|
+
error: ev.status === "failed" ? ev.error ?? "the suggestion call failed" : null
|
|
62
|
+
};
|
|
63
|
+
case "expand": return {
|
|
64
|
+
...state,
|
|
65
|
+
expanded: true
|
|
66
|
+
};
|
|
67
|
+
case "collapse": return {
|
|
68
|
+
...state,
|
|
69
|
+
expanded: false
|
|
70
|
+
};
|
|
71
|
+
case "autorun": return {
|
|
72
|
+
...state,
|
|
73
|
+
autorun: ev.on ? "armed" : "disarmed"
|
|
74
|
+
};
|
|
75
|
+
case "running": return {
|
|
76
|
+
...state,
|
|
77
|
+
running: ev.running
|
|
78
|
+
};
|
|
79
|
+
case "human/prompt": return {
|
|
80
|
+
...state,
|
|
81
|
+
autorun: "disarmed",
|
|
82
|
+
hiddenTurnSeq: state.turnSeq,
|
|
83
|
+
expanded: false
|
|
84
|
+
};
|
|
85
|
+
case "off": return {
|
|
86
|
+
...state,
|
|
87
|
+
status: "off",
|
|
88
|
+
text: null,
|
|
89
|
+
turnSeq: null,
|
|
90
|
+
error: null,
|
|
91
|
+
expanded: false,
|
|
92
|
+
autorun: "disarmed"
|
|
93
|
+
};
|
|
94
|
+
case "goal": return {
|
|
95
|
+
...state,
|
|
96
|
+
goalPhase: ev.phase,
|
|
97
|
+
autorun: ev.phase === "complete" ? "disarmed" : state.autorun
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
/** The status label. `failed` keeps the ready label so the error span is the delta, not a mystery row. */
|
|
103
|
+
function barLabel(s) {
|
|
104
|
+
if (s.status === "analyzing") return "Analyzing situation…";
|
|
105
|
+
if (s.status === "ready" || s.status === "failed") return "Recommended Next Step";
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function barVisible(s) {
|
|
109
|
+
if (s.status === "off") return false;
|
|
110
|
+
if (s.status === "ready" && (s.text === null || s.text === "")) return false;
|
|
111
|
+
if (s.hiddenTurnSeq !== null && s.hiddenTurnSeq === s.turnSeq) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/** May the bar submit its own offer? Armed, present, idle, not dismissed, and still serving a purpose. */
|
|
115
|
+
function shouldAutoFire(s) {
|
|
116
|
+
if (s.status !== "ready") return false;
|
|
117
|
+
if (s.text === null || s.text === "") return false;
|
|
118
|
+
if (s.intent === "goal-edit") return false;
|
|
119
|
+
if (s.goal !== null && s.goal.phase === "active" && s.goal.activation === "armed") return false;
|
|
120
|
+
if (s.autorun !== "armed") return false;
|
|
121
|
+
if (s.running) return false;
|
|
122
|
+
if (s.goalPhase === "complete") return false;
|
|
123
|
+
if (s.hiddenTurnSeq !== null && s.hiddenTurnSeq === s.turnSeq) return false;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/** The command the bar pulls. One string, so the client and the command plane cannot drift silently. */
|
|
127
|
+
const BAR_STATE_COMMAND = "/chapters-prompt state";
|
|
128
|
+
/** The bar's Run button: the server submits the on-screen offer as the human's own turn. */
|
|
129
|
+
const BAR_RUN_COMMAND = "/chapters-prompt run";
|
|
130
|
+
/**
|
|
131
|
+
* Parse the `state` verb's `k: v` lines into a pull event.
|
|
132
|
+
*
|
|
133
|
+
* One output serving the human and the component is the design (see `docs/contract.md`); this function is
|
|
134
|
+
* the cost of that choice, and it is kept honest: an unrecognised line is IGNORED, not guessed at, and a
|
|
135
|
+
* payload that yields no `status` is `null` so the caller keeps its last state rather than inventing `off`
|
|
136
|
+
* (which would hide a bar that is merely mid-reply). Indentation and the prompt's own lines are the reason
|
|
137
|
+
* the prompt is collected separately from the scalar fields — it is the only multi-line value.
|
|
138
|
+
*/
|
|
139
|
+
function parseBarState(text) {
|
|
140
|
+
const lines = text.split("\n");
|
|
141
|
+
const head = lines[0] ?? "";
|
|
142
|
+
const statusWord = head.startsWith("suggested next step:") ? head.slice(20).trim() : "";
|
|
143
|
+
const status = statusWord === "off" ? "off" : statusWord === "idle" ? "idle" : statusWord === "analyzing" ? "analyzing" : statusWord === "ready" ? "ready" : statusWord === "failed" ? "failed" : null;
|
|
144
|
+
if (status === null) return null;
|
|
145
|
+
const scalars = {};
|
|
146
|
+
const prompt = [];
|
|
147
|
+
let inPrompt = false;
|
|
148
|
+
for (const line of lines.slice(1)) {
|
|
149
|
+
if (inPrompt) {
|
|
150
|
+
prompt.push(line.replace(/^ {4}/, ""));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const m = /^ {2}([a-z]+): ?(.*)$/.exec(line);
|
|
154
|
+
if (m === null) continue;
|
|
155
|
+
const key = m[1], value = m[2].trim();
|
|
156
|
+
if (key === "prompt") {
|
|
157
|
+
inPrompt = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
scalars[key] !== void 0 ? scalars[key] += ` ${value}` : scalars[key] = value;
|
|
161
|
+
}
|
|
162
|
+
const turnRaw = scalars["turn"] ?? "";
|
|
163
|
+
const turnSeq = /^\d+$/.test(turnRaw) ? Number(turnRaw) : null;
|
|
164
|
+
const errorRaw = scalars["error"] ?? "";
|
|
165
|
+
return {
|
|
166
|
+
t: "polled",
|
|
167
|
+
intent: ((scalars["intent"] ?? "").split(" ")[0] ?? "") === "goal-edit" ? "goal-edit" : "worker-step",
|
|
168
|
+
status,
|
|
169
|
+
...status === "ready" ? {
|
|
170
|
+
text: prompt.join("\n"),
|
|
171
|
+
turnSeq
|
|
172
|
+
} : {},
|
|
173
|
+
...status === "failed" ? {
|
|
174
|
+
error: errorRaw,
|
|
175
|
+
turnSeq
|
|
176
|
+
} : {}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** The autorun flag, read from the same payload — the row is durable, the flag is not. */
|
|
180
|
+
function parseBarAutorun(text) {
|
|
181
|
+
const m = /^ {2}autorun: (armed|disarmed)\b/m.exec(text);
|
|
182
|
+
return m === null ? null : m[1];
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Does the host say this session is busy right now?
|
|
186
|
+
*
|
|
187
|
+
* The dock's props are HOOKS (`useSessionStatus`, `useChat`, `useInput`, …) — documented in
|
|
188
|
+
* docs/contract.md § "What a dock component is handed" — so a component that waits for a scalar
|
|
189
|
+
* `props.running` waits forever, which is precisely why Autorun declared itself unavailable while the host
|
|
190
|
+
* was publishing the signal one field away. Shapes differ across hosts and versions, so every form is folded
|
|
191
|
+
* here, in one pure function, with `undefined` (unknown) kept distinct from `false` (idle): the loop refuses
|
|
192
|
+
* to start a turn on an unknown, and that distinction is the whole safety property.
|
|
193
|
+
*/
|
|
194
|
+
function deriveRunning(status) {
|
|
195
|
+
if (status === void 0 || status === null) return void 0;
|
|
196
|
+
if (typeof status === "boolean") return status;
|
|
197
|
+
if (typeof status === "string") {
|
|
198
|
+
if (/^(running|streaming|busy|active)$/i.test(status)) return true;
|
|
199
|
+
if (/^(idle|done|stopped|finished|ready|waiting)$/i.test(status)) return false;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (typeof status === "object") {
|
|
203
|
+
const s = status;
|
|
204
|
+
if (s.running !== void 0) return typeof s.running === "boolean" ? s.running : void 0;
|
|
205
|
+
return deriveRunning(s.status ?? s.state ?? s.phase);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// src/client/prompt-dock.ts
|
|
209
|
+
/**
|
|
210
|
+
* The Suggested Next Step dock row (#113) — thin by design.
|
|
211
|
+
*
|
|
212
|
+
* Every decision lives in `prompt-bar.ts` where a row can pin it; this file only moves pixels and bytes
|
|
213
|
+
* between the host slot and that reducer. That split is the reason it exists: the ghost was rejected for
|
|
214
|
+
* behaviour that had no home, and "one turn, one offer" cannot be tested in a component.
|
|
215
|
+
*
|
|
216
|
+
* Placement is measured, not styled: `conversation.input.dock` is the host's full-width stack rendered
|
|
217
|
+
* ABOVE the composer card (todo `order: 0`, goal `10`, queue `20`), so `order: 30` puts this row under every
|
|
218
|
+
* other dock entry and directly above the input — and because it is a docked row rather than an overlay, it
|
|
219
|
+
* can neither cover the placeholder nor collide with the send buttons. The ghost's ugliest defect was
|
|
220
|
+
* structural, and the structure fixes it.
|
|
221
|
+
*
|
|
222
|
+
* The bar is PUSH-SHAPED, PULL-FED. A registrant can own a reactive observable (`hooks: { x }` → prop
|
|
223
|
+
* `useX`) — that is how the goal bar gets its `activation` — but a plugin cannot publish a projection, and
|
|
224
|
+
* the only plugin-reachable call path is `remote.commands.execute`. So the component pulls the `state` verb
|
|
225
|
+
* on edges it can actually observe. Stated here because the alternative is a reader assuming a subscription
|
|
226
|
+
* that does not exist.
|
|
227
|
+
*
|
|
228
|
+
* @module dsh-chapters/src/client/prompt-dock
|
|
229
|
+
*/
|
|
230
|
+
const CSS$1 = [
|
|
231
|
+
".dsh-chapters_promptDock{box-sizing:border-box;width:calc(100% - 4 * var(--dsh-composer-dock-inset));max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));margin:0 auto}",
|
|
232
|
+
".dsh-chapters_promptBar{isolation:isolate;box-sizing:border-box;width:100%;border-radius:var(--dsw-radius-md);height:36px;box-shadow:var(--dsw-elevation-panel);border:0;align-items:center;gap:10px;padding:4px 5px 4px 12px;display:flex;position:relative}",
|
|
233
|
+
".dsh-chapters_promptBar::before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:\"\";pointer-events:none;position:absolute;inset:0}",
|
|
234
|
+
".dsh-chapters_promptExpanded::before{align-items:flex-start;position:absolute;inset:0}",
|
|
235
|
+
".dsh-chapters_promptGlyph{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}",
|
|
236
|
+
".dsh-chapters_promptLabel{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px}",
|
|
237
|
+
".dsh-chapters_promptText{min-width:0;color:var(--dsw-alias-label-primary-dimmed);font-size:13px;line-height:20px;cursor:pointer;border:0;background:0 0;padding:0;text-align:left;flex:1}",
|
|
238
|
+
".dsh-chapters_promptEllipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;display:block}",
|
|
239
|
+
".dsh-chapters_promptFull{white-space:pre-wrap;overflow:auto;max-height:9rem;display:block}",
|
|
240
|
+
".dsh-chapters_promptError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:20px;flex:none;max-width:34%}",
|
|
241
|
+
".dsh-chapters_promptNote{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px;min-width:0}",
|
|
242
|
+
".dsh-chapters_promptIntent{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}",
|
|
243
|
+
".dsh-chapters_promptActions{flex:none;align-items:center;gap:10px;display:flex}",
|
|
244
|
+
".dsh-chapters_promptBtn{corner-shape:round;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;justify-content:center;align-items:center;padding:0;display:inline-flex;font-size:13px}",
|
|
245
|
+
".dsh-chapters_promptBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
|
|
246
|
+
".dsh-chapters_promptBtn:disabled{opacity:.4;cursor:default}"
|
|
247
|
+
].join("\n");
|
|
248
|
+
const CSS_ID = "dsh-chapters/prompt-dock.css";
|
|
249
|
+
const ensureCss = () => {
|
|
250
|
+
if (typeof document === "undefined") return;
|
|
251
|
+
if (document.getElementById(CSS_ID) !== null) return;
|
|
252
|
+
const style = document.createElement("style");
|
|
253
|
+
style.id = CSS_ID;
|
|
254
|
+
style.setAttribute("data-plugin-css", CSS_ID);
|
|
255
|
+
style.textContent = CSS$1;
|
|
256
|
+
document.head.appendChild(style);
|
|
257
|
+
};
|
|
258
|
+
/** The analysis glyph, drawn to the goal icon's metric (16 viewBox, stroke currentColor, width 1).
|
|
259
|
+
* A prism-and-arrow: the feature reads the situation and points at the next move. */
|
|
260
|
+
const Glyph = () => (0, react.createElement)("svg", {
|
|
261
|
+
viewBox: "0 0 16 16",
|
|
262
|
+
width: 14,
|
|
263
|
+
height: 14,
|
|
264
|
+
fill: "none",
|
|
265
|
+
"aria-hidden": "true",
|
|
266
|
+
"stroke-width": "1"
|
|
267
|
+
}, (0, react.createElement)("path", {
|
|
268
|
+
d: "M2.5 3.5h11L8 9.5 2.5 3.5Z",
|
|
269
|
+
stroke: "currentColor"
|
|
270
|
+
}), (0, react.createElement)("path", {
|
|
271
|
+
d: "M8 9.5V14",
|
|
272
|
+
stroke: "currentColor"
|
|
273
|
+
}), (0, react.createElement)("path", {
|
|
274
|
+
d: "M6 12l2 2 2-2",
|
|
275
|
+
stroke: "currentColor"
|
|
276
|
+
}));
|
|
277
|
+
/** One action button, labelled through the host tooltip exactly as the goal bar labels its own. */
|
|
278
|
+
const Action = (props) => {
|
|
279
|
+
const button = (0, react.createElement)("button", {
|
|
280
|
+
type: "button",
|
|
281
|
+
className: "dsh-chapters_promptBtn",
|
|
282
|
+
"aria-label": props.label,
|
|
283
|
+
...props.disabled === true ? { disabled: true } : {},
|
|
284
|
+
...props.title !== void 0 ? { title: props.title } : {},
|
|
285
|
+
onClick: props.onClick
|
|
286
|
+
}, props.glyph);
|
|
287
|
+
return (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
288
|
+
label: props.label,
|
|
289
|
+
side: "bottom",
|
|
290
|
+
children: button
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
/** Run is allowed whenever there is an offer; AUTORUN is what the intent and the goal gate. Keeping the
|
|
294
|
+
* human's own button enabled is deliberate — the point of the protocol is to route a goal edit to a keystroke,
|
|
295
|
+
* never to disable the human's ability to send it. */
|
|
296
|
+
/**
|
|
297
|
+
* The words the collapsed row shows, per state. `idle` and `analyzing` must say something, because an empty
|
|
298
|
+
* row is indistinguishable from a broken one — the whole reason this feature took a day to read: the bar was
|
|
299
|
+
* live, enabled, and rendering nothing but whitespace.
|
|
300
|
+
*/
|
|
301
|
+
function lineFor(state) {
|
|
302
|
+
if (state.status === "ready") return state.text ?? "";
|
|
303
|
+
if (state.status === "analyzing") return "Analyzing the session…";
|
|
304
|
+
if (state.status === "failed") return state.error !== null && state.error !== "" ? `Cannot recommend: ${state.error}` : "Cannot recommend a next step right now";
|
|
305
|
+
return "No suggestion yet — I will recommend one after your next turn";
|
|
306
|
+
}
|
|
307
|
+
const canRunHere = (state) => state.text !== null && state.text !== "";
|
|
308
|
+
/** How long a state pull may take before we call it failed. Generous on purpose: this is a local RPC that
|
|
309
|
+
* answers in milliseconds when the session is attachable, and the failure it guards against is a hang. */
|
|
310
|
+
const PULL_TIMEOUT_MS = 15e3;
|
|
311
|
+
/** Retry ceiling for the FIRST pull only, in attempts and spacing. A mount-time race must not be able to
|
|
312
|
+
* starve the bar forever, and a real `off` answer stops the loop on the first reply either way. */
|
|
313
|
+
/** Cadence of the keep-poll. A local RPC that answers in milliseconds; the cost is one command per
|
|
314
|
+
* interval, and it ends the moment the state is `off`. */
|
|
315
|
+
const POLL_SPACING_MS = 1e4;
|
|
316
|
+
function execWithTimeout(exec, line, ms) {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const timer = setTimeout(() => {
|
|
319
|
+
reject(/* @__PURE__ */ new Error(`no answer within ${String(ms)}ms`));
|
|
320
|
+
}, ms);
|
|
321
|
+
exec(line).then((text) => {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
resolve(text);
|
|
324
|
+
}, (error) => {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function PromptDock(props) {
|
|
331
|
+
const { sessionId, inputActions, exec } = props;
|
|
332
|
+
const [state, dispatch] = (0, react.useReducer)(promptBarReducer, initialBarState(), (s) => ({
|
|
333
|
+
...s,
|
|
334
|
+
running: props.running ?? true
|
|
335
|
+
}));
|
|
336
|
+
const pulling = (0, react.useRef)(false);
|
|
337
|
+
const probed = (0, react.useRef)(false);
|
|
338
|
+
const pulled = (0, react.useRef)(false);
|
|
339
|
+
const alive = (0, react.useRef)(true);
|
|
340
|
+
(0, react.useEffect)(() => () => {
|
|
341
|
+
alive.current = false;
|
|
342
|
+
}, []);
|
|
343
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
344
|
+
const answered = (0, react.useRef)(false);
|
|
345
|
+
const replyStatus = (0, react.useRef)("unknown");
|
|
346
|
+
const stateRef = (0, react.useRef)(initialBarState());
|
|
347
|
+
stateRef.current = state;
|
|
348
|
+
if (!probed.current) {
|
|
349
|
+
probed.current = true;
|
|
350
|
+
console.info(`[client:prompt] inputActions keys: ${Object.keys(inputActions ?? {}).join(",") || "NONE"}`);
|
|
351
|
+
}
|
|
352
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
353
|
+
if (exec === void 0 || pulling.current) return;
|
|
354
|
+
pulling.current = true;
|
|
355
|
+
try {
|
|
356
|
+
const text = await execWithTimeout(exec, BAR_STATE_COMMAND, PULL_TIMEOUT_MS);
|
|
357
|
+
try {
|
|
358
|
+
window.__dshPromptReply = text;
|
|
359
|
+
} catch {}
|
|
360
|
+
const ev = parseBarState(text);
|
|
361
|
+
if (ev !== null) {
|
|
362
|
+
answered.current = true;
|
|
363
|
+
replyStatus.current = ev.status ?? replyStatus.current;
|
|
364
|
+
dispatch(ev);
|
|
365
|
+
}
|
|
366
|
+
const autorun = parseBarAutorun(text);
|
|
367
|
+
if (autorun !== null) dispatch({
|
|
368
|
+
t: "autorun",
|
|
369
|
+
on: autorun === "armed"
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.warn(`[client:prompt] pull failed: ${String(error?.message ?? error).slice(0, 180)}`);
|
|
373
|
+
} finally {
|
|
374
|
+
pulling.current = false;
|
|
375
|
+
}
|
|
376
|
+
}, [exec]);
|
|
377
|
+
(0, react.useEffect)(() => {
|
|
378
|
+
ensureCss();
|
|
379
|
+
if (pulled.current) return;
|
|
380
|
+
pulled.current = true;
|
|
381
|
+
let timer;
|
|
382
|
+
const tick = async () => {
|
|
383
|
+
await refresh();
|
|
384
|
+
if (replyStatus.current === "off") return;
|
|
385
|
+
timer = setTimeout(() => {
|
|
386
|
+
tick();
|
|
387
|
+
}, POLL_SPACING_MS);
|
|
388
|
+
};
|
|
389
|
+
tick();
|
|
390
|
+
return () => {
|
|
391
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
392
|
+
};
|
|
393
|
+
}, [refresh]);
|
|
394
|
+
(0, react.useEffect)(() => {
|
|
395
|
+
dispatch({
|
|
396
|
+
t: "running",
|
|
397
|
+
running: props.running ?? true
|
|
398
|
+
});
|
|
399
|
+
}, [props.running]);
|
|
400
|
+
const run = (0, react.useCallback)(() => {
|
|
401
|
+
if (state.text === null || state.text === "" || exec === void 0) return;
|
|
402
|
+
setBusy(true);
|
|
403
|
+
exec(BAR_RUN_COMMAND).then((out) => {
|
|
404
|
+
if (!alive.current) return;
|
|
405
|
+
if (/^cannot run:/i.test(out.trim())) dispatch({
|
|
406
|
+
t: "polled",
|
|
407
|
+
status: "failed",
|
|
408
|
+
text: state.text,
|
|
409
|
+
error: out.trim().slice(0, 200),
|
|
410
|
+
turnSeq: state.turnSeq
|
|
411
|
+
});
|
|
412
|
+
else dispatch({ t: "collapse" });
|
|
413
|
+
}).catch((error) => {
|
|
414
|
+
console.warn(`[client:prompt] run failed: ${String(error?.message ?? error).slice(0, 160)}`);
|
|
415
|
+
}).finally(() => {
|
|
416
|
+
if (alive.current) setBusy(false);
|
|
417
|
+
});
|
|
418
|
+
}, [
|
|
419
|
+
exec,
|
|
420
|
+
state.text,
|
|
421
|
+
state.turnSeq
|
|
422
|
+
]);
|
|
423
|
+
(0, react.useEffect)(() => {
|
|
424
|
+
if (!shouldAutoFire(state)) return;
|
|
425
|
+
run();
|
|
426
|
+
}, [state, run]);
|
|
427
|
+
const sessionSnapshot = (props.useSession ?? (() => void 0))((snapshot) => snapshot);
|
|
428
|
+
const autorunDisabled = (props.running ?? deriveRunning(sessionSnapshot)) === void 0;
|
|
429
|
+
if (sessionId === void 0 || !barVisible(state)) return null;
|
|
430
|
+
const populate = () => {
|
|
431
|
+
console.info(`[client:prompt] populate expanded=${String(state.expanded)} hasInsertText=${String(typeof inputActions?.insertText)} hasSetDraft=${String(typeof inputActions?.setDraft)} textLen=${String(state.text?.length ?? -1)}`);
|
|
432
|
+
if (!state.expanded) {
|
|
433
|
+
dispatch({ t: "expand" });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (state.text === null || state.text === "") return;
|
|
437
|
+
if (typeof inputActions?.insertText === "function") {
|
|
438
|
+
try {
|
|
439
|
+
document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]")?.focus();
|
|
440
|
+
} catch {}
|
|
441
|
+
const refuse = (why) => {
|
|
442
|
+
console.warn(`[client:prompt] click-to-draft refused: ${why}`);
|
|
443
|
+
dispatch({
|
|
444
|
+
t: "note",
|
|
445
|
+
note: "This host does not let a plugin write the composer, so the prompt cannot be edited before sending — Run submits it as your turn"
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
inputActions.insertText(state.text);
|
|
450
|
+
const box = document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]");
|
|
451
|
+
const head = state.text.slice(0, 24);
|
|
452
|
+
if ((box?.innerText ?? "").indexOf(head) === -1) setTimeout(() => {
|
|
453
|
+
if ((document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]")?.innerText ?? "").indexOf(head) === -1) refuse("insertText reported success and left the editor empty");
|
|
454
|
+
}, 400);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
refuse(String(error?.message ?? error).slice(0, 160));
|
|
457
|
+
}
|
|
458
|
+
setTimeout(() => {
|
|
459
|
+
try {
|
|
460
|
+
const box = document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]");
|
|
461
|
+
console.info(`[client:prompt] after insertText: editorText=${String(box?.innerText?.length ?? "NO-BOX")} focused=${String(document.activeElement === box)}`);
|
|
462
|
+
} catch {}
|
|
463
|
+
}, 250);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (typeof inputActions?.setDraft === "function") {
|
|
467
|
+
inputActions.setDraft(state.text);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
console.warn("[client:prompt] click-to-draft refused: this host exposes neither insertText nor setDraft");
|
|
471
|
+
};
|
|
472
|
+
return (0, react.createElement)("div", {
|
|
473
|
+
className: "dsh-chapters_promptDock",
|
|
474
|
+
"data-prompt-bar": "true"
|
|
475
|
+
}, (0, react.createElement)("div", { className: `dsh-chapters_promptBar${state.expanded ? " dsh-chapters_promptExpanded" : ""}` }, (0, react.createElement)("span", { className: "dsh-chapters_promptGlyph" }, (0, react.createElement)(Glyph)), (0, react.createElement)("span", { className: "dsh-chapters_promptLabel" }, barLabel(state) ?? ""), ...state.intent === "goal-edit" ? [(0, react.createElement)("span", {
|
|
476
|
+
key: "intent",
|
|
477
|
+
className: "dsh-chapters_promptIntent",
|
|
478
|
+
"aria-label": "This step edits the goal"
|
|
479
|
+
}, "edits the goal · run it yourself")] : [], (0, react.createElement)("button", {
|
|
480
|
+
type: "button",
|
|
481
|
+
className: "dsh-chapters_promptText",
|
|
482
|
+
"aria-label": state.expanded ? "Put this suggested prompt in the input box" : "Show the full suggested prompt",
|
|
483
|
+
onClick: populate
|
|
484
|
+
}, state.expanded ? (0, react.createElement)("span", { className: "dsh-chapters_promptFull" }, state.text ?? "") : (0, react.createElement)("span", { className: "dsh-chapters_promptEllipsis" }, lineFor(state))), ...state.note === "" ? [] : [(0, react.createElement)("span", {
|
|
485
|
+
key: "note",
|
|
486
|
+
className: "dsh-chapters_promptNote",
|
|
487
|
+
role: "status"
|
|
488
|
+
}, state.note)], ...state.error === "" ? [] : [(0, react.createElement)("span", {
|
|
489
|
+
key: "err",
|
|
490
|
+
className: "dsh-chapters_promptError",
|
|
491
|
+
role: "alert"
|
|
492
|
+
}, state.error)], (0, react.createElement)("div", { className: "dsh-chapters_promptActions" }, (0, react.createElement)(Action, {
|
|
493
|
+
label: "Run the suggested next step",
|
|
494
|
+
glyph: "▶",
|
|
495
|
+
onClick: run,
|
|
496
|
+
disabled: busy,
|
|
497
|
+
...state.text === null || state.text === "" || !canRunHere(state) ? { disabled: true } : {},
|
|
498
|
+
...state.intent === "goal-edit" ? { title: "Goal edits need a human turn. Run still works; autorun will never do this one." } : {}
|
|
499
|
+
}), (0, react.createElement)(Action, {
|
|
500
|
+
label: state.autorun === "armed" ? "Autorun off" : "Autorun",
|
|
501
|
+
glyph: state.autorun === "armed" ? "⟳" : "⟲",
|
|
502
|
+
disabled: autorunDisabled,
|
|
503
|
+
...autorunDisabled ? { title: "Autorun needs the host to publish an idle signal; this build does not, so nothing would fire safely." } : {},
|
|
504
|
+
onClick: () => {
|
|
505
|
+
(async () => {
|
|
506
|
+
const arm = state.autorun !== "armed";
|
|
507
|
+
if (exec !== void 0) await exec(`/chapters-prompt autorun ${arm ? "on" : "off"}`);
|
|
508
|
+
dispatch({
|
|
509
|
+
t: "autorun",
|
|
510
|
+
on: arm
|
|
511
|
+
});
|
|
512
|
+
await refresh();
|
|
513
|
+
})();
|
|
514
|
+
}
|
|
515
|
+
}), (0, react.createElement)(Action, {
|
|
516
|
+
label: "Turn prompt suggestions off for this session",
|
|
517
|
+
glyph: "✕",
|
|
518
|
+
onClick: () => {
|
|
519
|
+
(async () => {
|
|
520
|
+
if (exec !== void 0) await exec("/chapters-prompt off");
|
|
521
|
+
dispatch({ t: "off" });
|
|
522
|
+
})();
|
|
523
|
+
}
|
|
524
|
+
}))));
|
|
525
|
+
}
|
|
526
|
+
// src/client/doors.ts
|
|
10
527
|
const DOOR_ACTIONS = [{
|
|
11
528
|
id: "chapters-fork",
|
|
12
529
|
order: 30,
|
|
@@ -275,105 +792,21 @@ window.__ModuleLoader__.load({
|
|
|
275
792
|
}
|
|
276
793
|
};
|
|
277
794
|
}
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/client/prompt-ghost.ts
|
|
280
|
-
/**
|
|
281
|
-
* The ONE visibility predicate, in the host's own terms: the shipped
|
|
282
|
-
* placeholder shows on `draft === '' && attachments.length === 0`
|
|
283
|
-
* (InputBar.tsx `showPlaceholder`) — whitespace-only is CONTENT, it hides the
|
|
284
|
-
* guidance. The ghost agrees exactly, so the two layers can never disagree
|
|
285
|
-
* about "empty", and a typed (or space-typed) draft hides ours instantly.
|
|
286
|
-
*/
|
|
287
|
-
function shouldShowGhost(phase, running, draft, hasAttachments, suggestion) {
|
|
288
|
-
return phase === "shown" && !running && !hasAttachments && draft === "" && suggestion !== null && suggestion.text !== "";
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* The gesture decision for a keydown while the ghost is visible:
|
|
292
|
-
* ArrowRight accepts, Escape dismisses, anything else is the host's business.
|
|
293
|
-
* Returns the action for the caller to apply (and whether to preventDefault).
|
|
294
|
-
*/
|
|
295
|
-
function ghostKeyAction(key) {
|
|
296
|
-
if (key === "ArrowRight") return "accept";
|
|
297
|
-
if (key === "Escape") return "dismiss";
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Execute `/chapters-prompt suggest` for one session. Never throws: every arm
|
|
302
|
-
* answers. `refused: true` marks a refusal worth retrying at the next boundary;
|
|
303
|
-
* a wire/registry failure is also a refusal, so one bad network cannot loop.
|
|
304
|
-
*/
|
|
305
|
-
function makePromptFetcher(remote) {
|
|
306
|
-
return async (sessionId) => {
|
|
307
|
-
if (remote.commands?.execute === void 0) return {
|
|
308
|
-
ok: false,
|
|
309
|
-
text: "commands remote unavailable",
|
|
310
|
-
refused: true
|
|
311
|
-
};
|
|
312
|
-
try {
|
|
313
|
-
const result = (await remote.commands.execute(sessionId, "/chapters-prompt suggest", [])).value?.result;
|
|
314
|
-
const text = typeof result?.text === "string" ? result.text : "";
|
|
315
|
-
if (result?.kind === "success" && text !== "") return {
|
|
316
|
-
ok: true,
|
|
317
|
-
text
|
|
318
|
-
};
|
|
319
|
-
return {
|
|
320
|
-
ok: false,
|
|
321
|
-
text,
|
|
322
|
-
refused: true
|
|
323
|
-
};
|
|
324
|
-
} catch (error) {
|
|
325
|
-
return {
|
|
326
|
-
ok: false,
|
|
327
|
-
text: String(error?.message ?? error).slice(0, 90),
|
|
328
|
-
refused: true
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
795
|
/**
|
|
334
|
-
* Execute
|
|
335
|
-
*
|
|
336
|
-
*
|
|
796
|
+
* Execute a host command line for one session and return its text, or '' when the command plane is
|
|
797
|
+
* unavailable. The bar's `state` pull and `off` write ride this, because it is the ONE place the client
|
|
798
|
+
* talks to the wire: the guard row "there is no second runner body in the client chrome" exists to keep
|
|
799
|
+
* `index.ts` as registration chrome, and a duplicated `commands.execute` in two files is how a wire
|
|
800
|
+
* behaviour drifts from its own test (#59's one-predicate rule, applied to the transport).
|
|
337
801
|
*/
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
} catch {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
};
|
|
802
|
+
async function runCommandText(remote, sessionId, line) {
|
|
803
|
+
const exec = remote?.commands?.execute;
|
|
804
|
+
if (exec === void 0) return "";
|
|
805
|
+
const res = await exec(sessionId, line, []);
|
|
806
|
+
if (res.ok !== true) return "";
|
|
807
|
+
return res.value?.result?.text ?? "";
|
|
348
808
|
}
|
|
349
|
-
|
|
350
|
-
* The boundary bookkeeping as a pure reducer so the component holds no logic:
|
|
351
|
-
* `runningEdge` reports the true→false transition of the session.
|
|
352
|
-
*/
|
|
353
|
-
function ghostReducer(state, event) {
|
|
354
|
-
switch (event.type) {
|
|
355
|
-
case "idle-edge": return {
|
|
356
|
-
phase: "idle",
|
|
357
|
-
boundary: state.boundary + 1
|
|
358
|
-
};
|
|
359
|
-
case "fetched":
|
|
360
|
-
if (state.phase !== "idle") return state;
|
|
361
|
-
return {
|
|
362
|
-
...state,
|
|
363
|
-
phase: event.ok ? "shown" : "refused"
|
|
364
|
-
};
|
|
365
|
-
case "accept": return {
|
|
366
|
-
...state,
|
|
367
|
-
phase: "accepted"
|
|
368
|
-
};
|
|
369
|
-
case "dismiss": return {
|
|
370
|
-
...state,
|
|
371
|
-
phase: "dismissed"
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
//#endregion
|
|
376
|
-
//#region src/client/index.ts
|
|
809
|
+
// src/client/index.ts
|
|
377
810
|
/**
|
|
378
811
|
* dsh-chapters client entry: the per-message door buttons.
|
|
379
812
|
*
|
|
@@ -425,11 +858,6 @@ window.__ModuleLoader__.load({
|
|
|
425
858
|
".dsh-chapters_forkAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
|
|
426
859
|
".dsh-chapters_forkAction:disabled{cursor:default;opacity:.4}",
|
|
427
860
|
".dsh-chapters_forkNote{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}",
|
|
428
|
-
".dsh-chapters_ghostWrap{position:absolute;top:2px;left:14px;right:14px;display:flex;justify-content:flex-start;pointer-events:none;z-index:1}",
|
|
429
|
-
".dsh-chapters_ghost{pointer-events:auto;display:inline-flex;align-items:center;gap:6px;max-width:min(70%,560px);padding:2px 10px;border:none;border-radius:12px;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-caption);font-size:12px;line-height:18px}",
|
|
430
|
-
".dsh-chapters_ghost:hover{color:var(--dsw-alias-label-secondary)}",
|
|
431
|
-
".dsh-chapters_ghostText{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}",
|
|
432
|
-
".dsh-chapters_ghostGlyph{flex:none;font-weight:600}",
|
|
433
861
|
"button[class*=\"_action\"][aria-label=\"Branch into a new conversation\"],button[class*=\"_action\"][aria-label=\"在新对话中分支\"]{display:none !important}"
|
|
434
862
|
].join("\n");
|
|
435
863
|
const CSS_TAG_ID = "dsh-chapters/assistant-action.css";
|
|
@@ -489,118 +917,6 @@ window.__ModuleLoader__.load({
|
|
|
489
917
|
}), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
|
|
490
918
|
};
|
|
491
919
|
}
|
|
492
|
-
function makeGhostView() {
|
|
493
|
-
return function ChaptersPromptGhost(props) {
|
|
494
|
-
const snap = props.useInput((s) => s);
|
|
495
|
-
const running = props.useSession((s) => s.running) ?? false;
|
|
496
|
-
const draft = snap?.draft ?? "";
|
|
497
|
-
const hasAttachments = (snap?.attachmentIds.length ?? 0) > 0;
|
|
498
|
-
const [state, setState] = (0, react.useState)({
|
|
499
|
-
phase: "idle",
|
|
500
|
-
boundary: 0
|
|
501
|
-
});
|
|
502
|
-
const [suggestion, setSuggestion] = (0, react.useState)(null);
|
|
503
|
-
const [model, setModel] = (0, react.useState)(null);
|
|
504
|
-
const [labelKnown, setLabelKnown] = (0, react.useState)(false);
|
|
505
|
-
const alive = (0, react.useRef)(true);
|
|
506
|
-
(0, react.useEffect)(() => () => {
|
|
507
|
-
alive.current = false;
|
|
508
|
-
}, []);
|
|
509
|
-
const dispatch = (0, react.useCallback)((ev) => {
|
|
510
|
-
setState((s) => ghostReducer(s, ev));
|
|
511
|
-
}, []);
|
|
512
|
-
const fetchLabelRef = (0, react.useRef)(props.fetchLabel);
|
|
513
|
-
fetchLabelRef.current = props.fetchLabel;
|
|
514
|
-
const labelFetched = (0, react.useRef)(false);
|
|
515
|
-
(0, react.useEffect)(() => {
|
|
516
|
-
if (labelFetched.current) return;
|
|
517
|
-
labelFetched.current = true;
|
|
518
|
-
fetchLabelRef.current().then((m) => {
|
|
519
|
-
if (!alive.current) return;
|
|
520
|
-
setModel(m);
|
|
521
|
-
setLabelKnown(true);
|
|
522
|
-
});
|
|
523
|
-
}, []);
|
|
524
|
-
const prevRunning = (0, react.useRef)(null);
|
|
525
|
-
(0, react.useEffect)(() => {
|
|
526
|
-
const first = prevRunning.current === null;
|
|
527
|
-
const edge = !running && (first || prevRunning.current === true);
|
|
528
|
-
prevRunning.current = running;
|
|
529
|
-
if (edge) dispatch({ type: "idle-edge" });
|
|
530
|
-
}, [running, dispatch]);
|
|
531
|
-
const fetchedFor = (0, react.useRef)(-1);
|
|
532
|
-
const { phase, boundary } = state;
|
|
533
|
-
(0, react.useEffect)(() => {
|
|
534
|
-
if (!labelKnown || model === null) return;
|
|
535
|
-
if (phase !== "idle" || fetchedFor.current === boundary) return;
|
|
536
|
-
fetchedFor.current = boundary;
|
|
537
|
-
props.fetchSuggestion().then((r) => {
|
|
538
|
-
if (!alive.current) return;
|
|
539
|
-
setSuggestion(r.ok ? {
|
|
540
|
-
text: r.text,
|
|
541
|
-
model: model !== null ? model : void 0
|
|
542
|
-
} : null);
|
|
543
|
-
dispatch({
|
|
544
|
-
type: "fetched",
|
|
545
|
-
ok: r.ok
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
}, [
|
|
549
|
-
phase,
|
|
550
|
-
boundary,
|
|
551
|
-
props,
|
|
552
|
-
dispatch,
|
|
553
|
-
labelKnown,
|
|
554
|
-
model
|
|
555
|
-
]);
|
|
556
|
-
const accept = (0, react.useCallback)(() => {
|
|
557
|
-
if (suggestion === null) return;
|
|
558
|
-
props.inputActions?.setDraft(suggestion.text);
|
|
559
|
-
dispatch({ type: "accept" });
|
|
560
|
-
}, [
|
|
561
|
-
suggestion,
|
|
562
|
-
props,
|
|
563
|
-
dispatch
|
|
564
|
-
]);
|
|
565
|
-
const visible = shouldShowGhost(state.phase, running, draft, hasAttachments, suggestion);
|
|
566
|
-
(0, react.useEffect)(() => {
|
|
567
|
-
if (!visible) return;
|
|
568
|
-
const onKey = (event) => {
|
|
569
|
-
const action = ghostKeyAction(event.key);
|
|
570
|
-
if (action === null) return;
|
|
571
|
-
const target = event.target;
|
|
572
|
-
if (target === null || typeof target.closest !== "function" || target.closest("[data-composer-input]") === null) return;
|
|
573
|
-
event.preventDefault();
|
|
574
|
-
event.stopPropagation();
|
|
575
|
-
if (action === "accept") accept();
|
|
576
|
-
else dispatch({ type: "dismiss" });
|
|
577
|
-
};
|
|
578
|
-
document.addEventListener("keydown", onKey, true);
|
|
579
|
-
return () => {
|
|
580
|
-
document.removeEventListener("keydown", onKey, true);
|
|
581
|
-
};
|
|
582
|
-
}, [
|
|
583
|
-
visible,
|
|
584
|
-
accept,
|
|
585
|
-
dispatch
|
|
586
|
-
]);
|
|
587
|
-
if (!visible || suggestion === null) return null;
|
|
588
|
-
const tip = `next step suggested${model !== null ? ` by ${model}` : ""} — → or click puts it in the draft, Esc dismisses`;
|
|
589
|
-
return (0, react.createElement)("span", { className: "dsh-chapters_ghostWrap" }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
590
|
-
label: tip,
|
|
591
|
-
side: "top",
|
|
592
|
-
children: (0, react.createElement)("button", {
|
|
593
|
-
type: "button",
|
|
594
|
-
className: "dsh-chapters_ghost",
|
|
595
|
-
"aria-label": "Accept the suggested next prompt",
|
|
596
|
-
onClick: accept
|
|
597
|
-
}, (0, react.createElement)("span", {
|
|
598
|
-
className: "dsh-chapters_ghostGlyph",
|
|
599
|
-
"aria-hidden": "true"
|
|
600
|
-
}, "→"), (0, react.createElement)("span", { className: "dsh-chapters_ghostText" }, suggestion.text))
|
|
601
|
-
}));
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
920
|
function apply(ctx) {
|
|
605
921
|
injectStyles();
|
|
606
922
|
const slots = ctx.slots;
|
|
@@ -643,20 +959,13 @@ window.__ModuleLoader__.load({
|
|
|
643
959
|
order: door.order,
|
|
644
960
|
inject: (sessionId) => ({ run: () => run(sessionId) })
|
|
645
961
|
}, component)));
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
order: 12,
|
|
653
|
-
inject: (sessionId) => ({
|
|
654
|
-
fetchSuggestion: () => fetchSuggestion(sessionId),
|
|
655
|
-
fetchLabel: () => fetchLabel(sessionId)
|
|
656
|
-
})
|
|
657
|
-
}, ghostView));
|
|
962
|
+
slots.inject("conversation.input.dock", () => slots.register({
|
|
963
|
+
name: "conversation.input.dock",
|
|
964
|
+
id: "chapters-prompt",
|
|
965
|
+
order: 30,
|
|
966
|
+
inject: (sessionId) => ({ exec: (line) => runCommandText(remote, sessionId, line) })
|
|
967
|
+
}, PromptDock));
|
|
658
968
|
}
|
|
659
|
-
//#endregion
|
|
660
969
|
exports.apply = apply;
|
|
661
970
|
exports.inject = inject;
|
|
662
971
|
return module.exports;
|