@treeseed/dsh-chapters 0.1.24 → 0.2.0
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/README.md +2 -1
- package/cordis.patch.yml +49 -1
- package/docs/architecture.md +1 -496
- package/docs/backlog.md +234 -4
- package/docs/contract.md +127 -1
- package/docs/development.md +248 -12
- package/docs/host-compaction-seam.md +108 -0
- package/docs/knowledge-repo.md +532 -16
- package/docs/project-state.md +323 -0
- package/docs/verify.md +217 -0
- package/lib/archive.d.ts +49 -1
- package/lib/archive.js +56 -2
- package/lib/archive.js.map +1 -1
- package/lib/arrival.js +24 -10
- package/lib/arrival.js.map +1 -1
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +74 -0
- package/lib/auto-save.js +162 -26
- package/lib/auto-save.js.map +1 -1
- package/lib/client.js +413 -11
- package/lib/client.js.map +1 -1
- package/lib/commands.d.ts +8 -1
- package/lib/commands.js +135 -7
- package/lib/commands.js.map +1 -1
- package/lib/compose.js +25 -6
- package/lib/compose.js.map +1 -1
- package/lib/continue-core.d.ts +24 -1
- package/lib/continue-core.js +41 -9
- package/lib/continue-core.js.map +1 -1
- package/lib/diag.d.ts +47 -0
- package/lib/diag.js +141 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +51 -1
- package/lib/engine-core.js +63 -7
- package/lib/engine-core.js.map +1 -1
- package/lib/engine.d.ts +93 -0
- package/lib/engine.js +477 -36
- package/lib/engine.js.map +1 -1
- package/lib/enrich-queue.js +28 -2
- package/lib/enrich-queue.js.map +1 -1
- package/lib/enrich-wire.d.ts +92 -2
- package/lib/enrich-wire.js +135 -17
- package/lib/enrich-wire.js.map +1 -1
- package/lib/enrich.d.ts +27 -2
- package/lib/enrich.js +114 -18
- package/lib/enrich.js.map +1 -1
- package/lib/index.d.ts +51 -0
- package/lib/index.js +180 -2
- package/lib/index.js.map +1 -1
- package/lib/indexing.d.ts +7 -0
- package/lib/indexing.js.map +1 -1
- package/lib/injections.d.ts +1 -0
- package/lib/injections.js +38 -0
- package/lib/injections.js.map +1 -1
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/prompt.d.ts +130 -0
- package/lib/prompt.js +255 -0
- package/lib/prompt.js.map +1 -0
- package/lib/registry.d.ts +39 -1
- package/lib/registry.js +76 -9
- package/lib/registry.js.map +1 -1
- package/lib/render.d.ts +31 -3
- package/lib/render.js +75 -3
- package/lib/render.js.map +1 -1
- package/lib/repair-run.d.ts +49 -0
- package/lib/repair-run.js +423 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +200 -0
- package/lib/repair.js +354 -0
- package/lib/repair.js.map +1 -0
- package/lib/rules-commands.d.ts +22 -0
- package/lib/rules-commands.js +177 -20
- package/lib/rules-commands.js.map +1 -1
- package/lib/rules.d.ts +184 -7
- package/lib/rules.js +312 -27
- package/lib/rules.js.map +1 -1
- package/lib/save-core.d.ts +31 -1
- package/lib/save-core.js +153 -17
- package/lib/save-core.js.map +1 -1
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/store.d.ts +22 -1
- package/lib/store.js +94 -17
- package/lib/store.js.map +1 -1
- package/lib/sync.js +13 -1
- package/lib/sync.js.map +1 -1
- package/lib/tools.d.ts +14 -0
- package/lib/tools.js +311 -32
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +12 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- package/package.json +9 -3
package/lib/client.js
CHANGED
|
@@ -24,20 +24,132 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
}];
|
|
25
25
|
/** Pull the child id out of either door's durable success text — both end in
|
|
26
26
|
* `… is session ch-…` (src/tools.ts), so this regex is door-agnostic. */
|
|
27
|
+
/**
|
|
28
|
+
* Which build of THIS file produced a door note (#77). When the live probe reported a roster
|
|
29
|
+
* trace of `missing → missing → missing` — meaning adoption should have fired three times — while
|
|
30
|
+
* showing no adoption outcome at all, the first question stopped being "why did the client refuse"
|
|
31
|
+
* and became "did the browser run the code I am reading?". A note that names its own build makes
|
|
32
|
+
* that a glance instead of an archaeology session into symlinked plugin trees.
|
|
33
|
+
*/
|
|
34
|
+
const DOOR_BUILD = "2026-10-22T23:55 door-build-stamp";
|
|
27
35
|
const CHILD_ID = /is session (ch-[\w-]+)/;
|
|
28
36
|
/** The quoted title the door gave the child, matched by its OWN noun so a
|
|
29
37
|
* refusal or an unrelated quote cannot pass for it. */
|
|
30
38
|
const doorTitle = (noun) => new RegExp(`${noun} \\u201C([^\\u201D]+)\\u201D is session`);
|
|
31
|
-
|
|
39
|
+
/**
|
|
40
|
+
* A refusal reason that can never render as nothing. `String(err.message ?? err)` is `''` for a
|
|
41
|
+
* host error built with an empty message, and an empty reason disappears from the note — the
|
|
42
|
+
* reader then concludes "no refusal happened", which is the opposite of the truth (#77).
|
|
43
|
+
*/
|
|
44
|
+
function refusalReason(error) {
|
|
45
|
+
const trimmed = (error instanceof Error ? error.message : String(error)).trim();
|
|
46
|
+
const named = trimmed === "" ? error instanceof Error && error.name.trim() !== "" ? error.name : "" : trimmed;
|
|
47
|
+
return (named === "" ? error === null ? "null thrown" : typeof error : named).replace(/\s+/g, " ").slice(0, 90);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Is the child in the client's OWN roster? This is the question that separates two very
|
|
51
|
+
* different failures, and the door used to report the symptom without it:
|
|
52
|
+
* - the client never learned the session exists (roster: missing) → the fix is server-side
|
|
53
|
+
* publication, and no amount of clicking `open` will ever work;
|
|
54
|
+
* - the client knows it and `open` still did nothing (roster: contains) → a navigation bug.
|
|
55
|
+
* Measured host 0.2.0-rc.2: the sessions service exposes `projectList`, `refresh`, `retain`,
|
|
56
|
+
* `handleSessionAdded` — the roster is a projection the client fills from control frames.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* The client's own session ids, or `null` when the store cannot be read. ONE reader for both
|
|
60
|
+
* the note and the adoption decision, so the door never acts on a verdict it did not report.
|
|
61
|
+
*/
|
|
62
|
+
function rosterIds(sessions) {
|
|
63
|
+
const store = sessions?.list;
|
|
64
|
+
if (typeof store?.getSnapshot !== "function") return null;
|
|
65
|
+
const snap = store.getSnapshot();
|
|
66
|
+
const ids = /* @__PURE__ */ new Set();
|
|
67
|
+
if (Array.isArray(snap?.ids)) for (const id of snap.ids) ids.add(String(id));
|
|
68
|
+
else if (snap?.byId !== void 0) for (const k of Object.keys(snap.byId)) ids.add(k);
|
|
69
|
+
else return null;
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
function rosterNote(sessions, child) {
|
|
73
|
+
try {
|
|
74
|
+
const ids = rosterIds(sessions);
|
|
75
|
+
if (ids === null) return ` (roster: ${typeof (sessions?.list)?.getSnapshot !== "function" ? "no list store on the service" : "snapshot unreadable"} — presence of ${child.slice(0, 11)}… is UNKNOWN, not absent)`;
|
|
76
|
+
if (ids.size === 0) return ` (roster: empty — presence of ${child.slice(0, 11)}… is UNKNOWN, not absent)`;
|
|
77
|
+
const n = ids.size;
|
|
78
|
+
return ids.has(child) ? ` (roster: contains ${child.slice(0, 11)}…, ${String(n)} sessions — open() is the failure)` : ` (roster: MISSING, ${String(n)} sessions — the client never catalogued this session)`;
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The service's own method names, as a hint attached to a failure note (never a guess). */
|
|
84
|
+
function verbsNote(sessions) {
|
|
85
|
+
try {
|
|
86
|
+
const svc = sessions;
|
|
87
|
+
if (svc === null || svc === void 0 || typeof svc !== "object") return " (sessions service: absent)";
|
|
88
|
+
const found = /* @__PURE__ */ new Set();
|
|
89
|
+
let node = svc;
|
|
90
|
+
for (let depth = 0; depth < 6 && node !== null && node !== void 0 && node !== Object.prototype; depth++) {
|
|
91
|
+
for (const key of Object.getOwnPropertyNames(node)) {
|
|
92
|
+
if (key === "constructor") continue;
|
|
93
|
+
try {
|
|
94
|
+
if (typeof svc[key] === "function") found.add(key);
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
node = Object.getPrototypeOf(node);
|
|
98
|
+
}
|
|
99
|
+
const verbs = [...found].sort().slice(0, 24);
|
|
100
|
+
return verbs.length === 0 ? " (sessions service: no methods on the chain)" : ` (sessions verbs: ${verbs.join(", ")})`;
|
|
101
|
+
} catch {
|
|
102
|
+
return " (sessions service: unreadable)";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function openByService(sessions, sessionId, adopt, nav) {
|
|
32
106
|
return (async () => {
|
|
33
107
|
const svc = sessions;
|
|
34
|
-
if (
|
|
108
|
+
if (svc === void 0) return false;
|
|
109
|
+
const navigate = typeof nav?.openSession === "function" ? (id) => nav.openSession(id) : typeof svc?.open === "function" ? (id) => svc.open(id) : null;
|
|
110
|
+
if (navigate === null) {
|
|
111
|
+
adopt?.onStep?.("no-verb");
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
35
114
|
try {
|
|
36
115
|
await svc.refresh?.();
|
|
37
116
|
} catch {}
|
|
117
|
+
try {
|
|
118
|
+
svc.projectList?.();
|
|
119
|
+
} catch {}
|
|
120
|
+
if (adopt?.needed === true && typeof svc.create === "function") {
|
|
121
|
+
adopt.onStep?.("enter-create");
|
|
122
|
+
try {
|
|
123
|
+
await svc.create({ sessionId });
|
|
124
|
+
adopt.onStep?.("settle-create");
|
|
125
|
+
try {
|
|
126
|
+
svc.projectList?.();
|
|
127
|
+
} catch {}
|
|
128
|
+
const after = rosterIds(sessions);
|
|
129
|
+
adopt.onAfter?.(after === null ? null : after.has(sessionId));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
adopt.onStep?.("refuse-create");
|
|
132
|
+
adopt.onRefused(refusalReason(error));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (adopt?.needed === true && typeof svc.using === "function" && rosterIds(sessions)?.has(sessionId) === false) {
|
|
136
|
+
adopt.onStep?.("enter-using");
|
|
137
|
+
try {
|
|
138
|
+
await svc.using(sessionId, { source: "dsh-chapters-door" }, async () => void 0);
|
|
139
|
+
adopt.onStep?.("settle-using");
|
|
140
|
+
try {
|
|
141
|
+
svc.projectList?.();
|
|
142
|
+
} catch {}
|
|
143
|
+
const seen = rosterIds(sessions);
|
|
144
|
+
adopt.onAfter?.(seen === null ? null : seen.has(sessionId));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
adopt.onStep?.("refuse-using");
|
|
147
|
+
adopt.onSubscribeRefused?.(refusalReason(error));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
38
150
|
const started = Date.now();
|
|
39
151
|
for (;;) try {
|
|
40
|
-
|
|
152
|
+
navigate(sessionId);
|
|
41
153
|
return true;
|
|
42
154
|
} catch {
|
|
43
155
|
if (Date.now() - started > 4e3) return false;
|
|
@@ -77,7 +189,7 @@ window.__ModuleLoader__.load({
|
|
|
77
189
|
}
|
|
78
190
|
return tryClick();
|
|
79
191
|
}
|
|
80
|
-
function makeDoorRunner(remote, sessions, door) {
|
|
192
|
+
function makeDoorRunner(remote, sessions, door, verifySwitched, opts) {
|
|
81
193
|
return async function run(sessionId) {
|
|
82
194
|
try {
|
|
83
195
|
if (remote.commands?.execute === void 0) return {
|
|
@@ -100,18 +212,60 @@ window.__ModuleLoader__.load({
|
|
|
100
212
|
ok: true,
|
|
101
213
|
message: `${door.noun} created — appearing in the sidebar`
|
|
102
214
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
215
|
+
const switchedOnce = async () => {
|
|
216
|
+
if (verifySwitched === void 0) return true;
|
|
217
|
+
try {
|
|
218
|
+
return await verifySwitched(child);
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
106
222
|
};
|
|
223
|
+
const maxAttempts = typeof sessions?.refresh === "function" ? opts?.attempts ?? 3 : 1;
|
|
224
|
+
let tried = 0;
|
|
225
|
+
let adoptRefusal = "";
|
|
226
|
+
let subscribeRefusal = "";
|
|
227
|
+
const rosterTrace = [];
|
|
228
|
+
const steps = [];
|
|
229
|
+
let adoptSaw;
|
|
230
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
231
|
+
tried += 1;
|
|
232
|
+
const ids = rosterIds(sessions);
|
|
233
|
+
const needed = ids !== null && !ids.has(child);
|
|
234
|
+
const svcShape = sessions;
|
|
235
|
+
rosterTrace.push(`${ids === null ? "unknown" : ids.size === 0 ? "empty" : ids.has(child) ? "has" : "missing"}${typeof svcShape?.create === "function" ? "+c" : "-c"}${typeof svcShape?.using === "function" ? "+u" : "-u"}${typeof svcShape?.open === "function" ? "+o" : "-o"}${typeof opts?.navigator?.openSession === "function" ? "+w" : "-w"}`);
|
|
236
|
+
if (await openByService(sessions, child, {
|
|
237
|
+
needed,
|
|
238
|
+
onRefused: (reason) => {
|
|
239
|
+
adoptRefusal = reason;
|
|
240
|
+
},
|
|
241
|
+
onSubscribeRefused: (reason) => {
|
|
242
|
+
subscribeRefusal = reason;
|
|
243
|
+
},
|
|
244
|
+
onAfter: (seen) => {
|
|
245
|
+
adoptSaw = seen;
|
|
246
|
+
},
|
|
247
|
+
onStep: (step) => {
|
|
248
|
+
if (!steps.includes(step)) steps.push(step);
|
|
249
|
+
}
|
|
250
|
+
}, opts?.navigator) && await switchedOnce()) return {
|
|
251
|
+
ok: true,
|
|
252
|
+
message: ""
|
|
253
|
+
};
|
|
254
|
+
if (i + 1 < maxAttempts) await new Promise((r) => {
|
|
255
|
+
setTimeout(r, opts?.attemptPauseMs ?? 300).unref?.();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
107
258
|
const title = doorTitle(door.noun).exec(text)?.[1];
|
|
108
|
-
if (title !== void 0 && await openBranchRow(title)) return {
|
|
259
|
+
if (title !== void 0 && await openBranchRow(title) && await switchedOnce()) return {
|
|
109
260
|
ok: true,
|
|
110
261
|
message: ""
|
|
111
262
|
};
|
|
112
|
-
return {
|
|
263
|
+
return verifySwitched === void 0 ? {
|
|
113
264
|
ok: true,
|
|
114
265
|
message: `${door.noun} created — open it in the sidebar`
|
|
266
|
+
} : {
|
|
267
|
+
ok: true,
|
|
268
|
+
message: `${door.noun} created — the app did not switch after ${String(tried)} attempt${tried === 1 ? "" : "s"}; open it from the sidebar [roster per attempt: ${rosterTrace.join(" → ")}] [tried: ${steps.length === 0 ? "none" : steps.includes("no-verb") ? "no open or openSession verb on any injected service" : steps.join(", ")}] (build: ${DOOR_BUILD})${adoptRefusal === "" ? "" : ` (adoption refused: ${adoptRefusal})`}${subscribeRefusal === "" ? "" : ` (subscribe refused: ${subscribeRefusal})`}${adoptSaw === void 0 ? "" : ` (right after adopt: ${adoptSaw ? "contains" : "missing"} ${child.slice(0, 15)})`}${rosterNote(sessions, child)}${verbsNote(sessions)}`
|
|
115
269
|
};
|
|
116
270
|
} catch (error) {
|
|
117
271
|
return {
|
|
@@ -122,6 +276,103 @@ window.__ModuleLoader__.load({
|
|
|
122
276
|
};
|
|
123
277
|
}
|
|
124
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
|
+
/**
|
|
334
|
+
* Execute `/chapters-prompt model` once per mount and pull the spec out of the
|
|
335
|
+
* status sentence for the hover label. Absent/unparseable ⇒ no label (the chip
|
|
336
|
+
* still works) — a display fact is never a dependency.
|
|
337
|
+
*/
|
|
338
|
+
function makeModelLabelFetcher(remote) {
|
|
339
|
+
return async (sessionId) => {
|
|
340
|
+
if (remote.commands?.execute === void 0) return null;
|
|
341
|
+
try {
|
|
342
|
+
const res = await remote.commands.execute(sessionId, "/chapters-prompt model", []);
|
|
343
|
+
return (res.value?.result?.kind === "success" ? res.value.result.text ?? "" : "").match(/^prompt model: (\S+(?: \S+)*) \(.*\) — ON$/)?.[1] ?? null;
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
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
|
|
125
376
|
//#region src/client/index.ts
|
|
126
377
|
/**
|
|
127
378
|
* dsh-chapters client entry: the per-message door buttons.
|
|
@@ -152,7 +403,8 @@ window.__ModuleLoader__.load({
|
|
|
152
403
|
"slots",
|
|
153
404
|
"remote",
|
|
154
405
|
"remote.commands",
|
|
155
|
-
"sessions"
|
|
406
|
+
"sessions",
|
|
407
|
+
"uiWorkspace"
|
|
156
408
|
];
|
|
157
409
|
/**
|
|
158
410
|
* Style parity with the native IconActions row: rules copied verbatim from the
|
|
@@ -173,6 +425,11 @@ window.__ModuleLoader__.load({
|
|
|
173
425
|
".dsh-chapters_forkAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
|
|
174
426
|
".dsh-chapters_forkAction:disabled{cursor:default;opacity:.4}",
|
|
175
427
|
".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}",
|
|
176
433
|
"button[class*=\"_action\"][aria-label=\"Branch into a new conversation\"],button[class*=\"_action\"][aria-label=\"在新对话中分支\"]{display:none !important}"
|
|
177
434
|
].join("\n");
|
|
178
435
|
const CSS_TAG_ID = "dsh-chapters/assistant-action.css";
|
|
@@ -232,6 +489,118 @@ window.__ModuleLoader__.load({
|
|
|
232
489
|
}), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
|
|
233
490
|
};
|
|
234
491
|
}
|
|
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
|
+
}
|
|
235
604
|
function apply(ctx) {
|
|
236
605
|
injectStyles();
|
|
237
606
|
const slots = ctx.slots;
|
|
@@ -242,9 +611,30 @@ window.__ModuleLoader__.load({
|
|
|
242
611
|
} catch {
|
|
243
612
|
sessions = void 0;
|
|
244
613
|
}
|
|
614
|
+
let uiWorkspace;
|
|
615
|
+
try {
|
|
616
|
+
uiWorkspace = ctx.get("uiWorkspace");
|
|
617
|
+
} catch {
|
|
618
|
+
uiWorkspace = void 0;
|
|
619
|
+
}
|
|
620
|
+
const currentSessionId = () => {
|
|
621
|
+
try {
|
|
622
|
+
const raw = window.localStorage.getItem("dsh.sessions.current");
|
|
623
|
+
if (raw === null) return null;
|
|
624
|
+
return JSON.parse(raw)?.sessionId ?? null;
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
245
629
|
const rows = DOOR_ACTIONS.map((door) => ({
|
|
246
630
|
door,
|
|
247
|
-
run: makeDoorRunner(remote, sessions, door)
|
|
631
|
+
run: makeDoorRunner(remote, sessions, door, async (child) => {
|
|
632
|
+
for (let i = 0; i < 8; i++) {
|
|
633
|
+
if (currentSessionId() === child) return true;
|
|
634
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
635
|
+
}
|
|
636
|
+
return currentSessionId() === child;
|
|
637
|
+
}, { ...uiWorkspace !== void 0 ? { navigator: uiWorkspace } : {} }),
|
|
248
638
|
component: makeDoorAction(door)
|
|
249
639
|
}));
|
|
250
640
|
slots.inject("conversation.chat.assistant-actions", () => rows.map(({ door, run, component }) => slots.register({
|
|
@@ -253,6 +643,18 @@ window.__ModuleLoader__.load({
|
|
|
253
643
|
order: door.order,
|
|
254
644
|
inject: (sessionId) => ({ run: () => run(sessionId) })
|
|
255
645
|
}, component)));
|
|
646
|
+
const ghostView = makeGhostView();
|
|
647
|
+
const fetchSuggestion = makePromptFetcher(remote);
|
|
648
|
+
const fetchLabel = makeModelLabelFetcher(remote);
|
|
649
|
+
slots.inject("conversation.input.overlay", () => slots.register({
|
|
650
|
+
name: "conversation.input.overlay",
|
|
651
|
+
id: "chapters-prompt-ghost",
|
|
652
|
+
order: 12,
|
|
653
|
+
inject: (sessionId) => ({
|
|
654
|
+
fetchSuggestion: () => fetchSuggestion(sessionId),
|
|
655
|
+
fetchLabel: () => fetchLabel(sessionId)
|
|
656
|
+
})
|
|
657
|
+
}, ghostView));
|
|
256
658
|
}
|
|
257
659
|
//#endregion
|
|
258
660
|
exports.apply = apply;
|