@khorsheed/dsh-client-message-tools 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.en.md +82 -0
- package/README.i18n.yaml +6 -0
- package/README.md +82 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +5824 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +644 -0
- package/lib/invariant.js +27 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.host.tsbuildinfo +1 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +250 -0
- package/lib/typert.remote-client.d.ts +26 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +154 -0
- package/lib/types/client/ModelChip.d.ts +29 -0
- package/lib/types/client/ModelChip.d.ts.map +1 -0
- package/lib/types/client/ModelChip.js +86 -0
- package/lib/types/client/RestoredMessageView.d.ts +17 -0
- package/lib/types/client/RestoredMessageView.d.ts.map +1 -0
- package/lib/types/client/RestoredMessageView.js +9 -0
- package/lib/types/client/UserMessageView.d.ts +16 -0
- package/lib/types/client/UserMessageView.d.ts.map +1 -0
- package/lib/types/client/UserMessageView.js +172 -0
- package/lib/types/client/WithdrawnDividerView.d.ts +17 -0
- package/lib/types/client/WithdrawnDividerView.d.ts.map +1 -0
- package/lib/types/client/WithdrawnDividerView.js +59 -0
- package/lib/types/client/backfill.d.ts +19 -0
- package/lib/types/client/backfill.d.ts.map +1 -0
- package/lib/types/client/backfill.js +20 -0
- package/lib/types/client/dom-hider.d.ts +61 -0
- package/lib/types/client/dom-hider.d.ts.map +1 -0
- package/lib/types/client/dom-hider.js +195 -0
- package/lib/types/client/edit-in-place.d.ts +107 -0
- package/lib/types/client/edit-in-place.d.ts.map +1 -0
- package/lib/types/client/edit-in-place.js +97 -0
- package/lib/types/client/icons.d.ts +23 -0
- package/lib/types/client/icons.d.ts.map +1 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +187 -0
- package/lib/types/client/locales.d.ts +74 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +65 -0
- package/lib/types/client/slots.d.ts +44 -0
- package/lib/types/client/slots.d.ts.map +1 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/client/withdraw-backfill.d.ts +25 -0
- package/lib/types/client/withdraw-backfill.d.ts.map +1 -0
- package/lib/types/client/withdraw-backfill.js +21 -0
- package/lib/types/client/withdrawn-node.d.ts +163 -0
- package/lib/types/client/withdrawn-node.d.ts.map +1 -0
- package/lib/types/client/withdrawn-node.js +297 -0
- package/lib/types/index.d.ts +69 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +175 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +25 -0
- package/lib/types/marker.d.ts +94 -0
- package/lib/types/marker.d.ts.map +1 -0
- package/lib/types/marker.js +131 -0
- package/lib/types/types.d.ts +116 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +1 -0
- package/lib/types/withdraw.d.ts +112 -0
- package/lib/types/withdraw.d.ts.map +1 -0
- package/lib/types/withdraw.js +279 -0
- package/package.json +108 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
|
|
2
|
+
/** Why a withdrawal cannot be planned. */
|
|
3
|
+
export type WithdrawalPlanFailure = 'target-not-found' | 'not-a-user-message' | 'already-withdrawn';
|
|
4
|
+
/** The validated replacement intent for one withdrawal. */
|
|
5
|
+
export interface WithdrawalPlan {
|
|
6
|
+
/** Seq of the withdrawn user message (a current surface node). */
|
|
7
|
+
readonly start: number;
|
|
8
|
+
/** Seq of the last current surface node; the span replaces [start, end]. */
|
|
9
|
+
readonly end: number;
|
|
10
|
+
/** Every shadowed surface node, in surface order (surface validation requires the complete set). */
|
|
11
|
+
readonly sourceEventSeqs: readonly number[];
|
|
12
|
+
}
|
|
13
|
+
/** Planning outcome: a replacement intent or a failure code. */
|
|
14
|
+
export type WithdrawalPlanResult = {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly plan: WithdrawalPlan;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ok: false;
|
|
19
|
+
readonly code: WithdrawalPlanFailure;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Plan a withdrawal: the target must be an editable user message (original,
|
|
23
|
+
* restore replay, or edit replacement — see `isEditableTarget`) still present
|
|
24
|
+
* on the live surface; the span covers it and every surface node after it.
|
|
25
|
+
* @param events - the full session log (seq === index).
|
|
26
|
+
* @param surfaceNodes - the current model-visible surface seqs, in order.
|
|
27
|
+
* @param targetSeq - seq of the user message to withdraw.
|
|
28
|
+
* @returns the replacement intent, or the failure code.
|
|
29
|
+
*/
|
|
30
|
+
export declare function planWithdrawal(events: readonly SessionEvent[], surfaceNodes: readonly number[], targetSeq: number): WithdrawalPlanResult;
|
|
31
|
+
/** Why an edit cannot be planned. */
|
|
32
|
+
export type EditPlanFailure = 'target-not-found' | 'not-a-user-message' | 'already-withdrawn' | 'empty-text';
|
|
33
|
+
/** Planning outcome: a replacement intent or a failure code. */
|
|
34
|
+
export type EditPlanResult = {
|
|
35
|
+
readonly ok: true;
|
|
36
|
+
readonly plan: WithdrawalPlan;
|
|
37
|
+
} | {
|
|
38
|
+
readonly ok: false;
|
|
39
|
+
readonly code: EditPlanFailure;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Plan an edit: the target must be an editable user message still present on
|
|
43
|
+
* the live surface, and the new text must be non-blank. The span covers the
|
|
44
|
+
* target and every surface node after it — editing an old message discards
|
|
45
|
+
* what followed it. The replacement's content is the new text (no
|
|
46
|
+
* placeholder), so the model context reads the edited message in place.
|
|
47
|
+
* @param events - the full session log (seq === index).
|
|
48
|
+
* @param surfaceNodes - the current model-visible surface seqs, in order.
|
|
49
|
+
* @param targetSeq - seq of the user message to edit.
|
|
50
|
+
* @param text - the edited text.
|
|
51
|
+
* @returns the replacement intent, or the failure code.
|
|
52
|
+
*/
|
|
53
|
+
export declare function planEdit(events: readonly SessionEvent[], surfaceNodes: readonly number[], targetSeq: number, text: string): EditPlanResult;
|
|
54
|
+
/** Why a restore cannot be planned. */
|
|
55
|
+
export type RestorePlanFailure = 'target-not-found' | 'not-a-user-message' | 'not-withdrawn';
|
|
56
|
+
/**
|
|
57
|
+
* One replayed entry of a restored span, in original order. User entries
|
|
58
|
+
* replay content blocks verbatim (originals, edit replacements — whose
|
|
59
|
+
* content IS the last edit's new text — and earlier restore replays);
|
|
60
|
+
* assistant entries carry the reply's joined text, framed for the model.
|
|
61
|
+
*/
|
|
62
|
+
export type RestoreReplayEntry = {
|
|
63
|
+
readonly role: 'user';
|
|
64
|
+
/** The entry's content blocks, replayed verbatim. */
|
|
65
|
+
readonly content: SessionEvent<'user/message'>['data']['content'];
|
|
66
|
+
/** Seq of the original event this entry replays. */
|
|
67
|
+
readonly sourceSeq: number;
|
|
68
|
+
} | {
|
|
69
|
+
readonly role: 'assistant';
|
|
70
|
+
/** The model-facing text (frame included — see RESTORED_ASSISTANT_NOTICE). */
|
|
71
|
+
readonly text: string;
|
|
72
|
+
/** Seq of the original event this entry replays. */
|
|
73
|
+
readonly sourceSeq: number;
|
|
74
|
+
};
|
|
75
|
+
/** The validated restore intent for one withdrawn message. */
|
|
76
|
+
export interface RestorePlan {
|
|
77
|
+
/**
|
|
78
|
+
* Every replayable entry of the withdrawn span, in original (surface)
|
|
79
|
+
* order; the host appends them back-to-back at the tail. Never empty: the
|
|
80
|
+
* target message itself always replays. Tool calls/results never replay —
|
|
81
|
+
* the call/result pairing cannot be re-entered and their side effects are
|
|
82
|
+
* not replayable; the assistant text already summarizes them.
|
|
83
|
+
*/
|
|
84
|
+
readonly entries: readonly RestoreReplayEntry[];
|
|
85
|
+
}
|
|
86
|
+
/** Restore planning outcome: a restore intent or a failure code. */
|
|
87
|
+
export type RestorePlanResult = {
|
|
88
|
+
readonly ok: true;
|
|
89
|
+
readonly plan: RestorePlan;
|
|
90
|
+
} | {
|
|
91
|
+
readonly ok: false;
|
|
92
|
+
readonly code: RestorePlanFailure;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Plan a restore: the target must be an editable user message (the same
|
|
96
|
+
* vocabulary as withdrawal — original, restore replay, or edit replacement)
|
|
97
|
+
* that is no longer on the live surface (i.e. withdrawn). Restoring an edit
|
|
98
|
+
* replacement replays the span's last edit text — the replacement's content
|
|
99
|
+
* IS the new text. The surface model is positional — a replaced span folds
|
|
100
|
+
* into exactly one node — so the withdrawn span can never be put back in
|
|
101
|
+
* place; restore replays the span's replayable content (user messages, the
|
|
102
|
+
* last edit's new text, assistant text) as fresh tail messages in original
|
|
103
|
+
* order. When no message-tools replacement cites the target (a foreign
|
|
104
|
+
* shadowing, e.g. compaction), only the target message replays — the span
|
|
105
|
+
* boundary is never re-guessed.
|
|
106
|
+
* @param events - the full session log (seq === index).
|
|
107
|
+
* @param surfaceNodes - the current model-visible surface seqs.
|
|
108
|
+
* @param targetSeq - seq of the withdrawn user message to restore.
|
|
109
|
+
* @returns the restore intent, or the failure code.
|
|
110
|
+
*/
|
|
111
|
+
export declare function planRestore(events: readonly SessionEvent[], surfaceNodes: readonly number[], targetSeq: number): RestorePlanResult;
|
|
112
|
+
//# sourceMappingURL=withdraw.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withdraw.d.ts","sourceRoot":"","sources":["../../src/withdraw.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Withdrawal planning: a pure fold from the session log and the live surface
|
|
3
|
+
* to the replacement intent. Kept free of cordis and Session internals so the
|
|
4
|
+
* unit tests drive it directly; the service only validates and appends.
|
|
5
|
+
* @module @khorsheed/dsh-client-message-tools/withdraw
|
|
6
|
+
*/
|
|
7
|
+
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
|
|
8
|
+
import { RESTORED_ASSISTANT_NOTICE, isMessageToolsEdit, isMessageToolsReplacement, isMessageToolsRestore, isMessageToolsRestoreAssistant, } from './marker.js';
|
|
9
|
+
/**
|
|
10
|
+
* Whether a target seq names an editable user message: an append-surface
|
|
11
|
+
* user-source message (an original), a message-tools restore replay (a
|
|
12
|
+
* restored row withdraws and edits like the message it replays), or a
|
|
13
|
+
* message-tools edit replacement (edit chains — the previous edit's
|
|
14
|
+
* replacement is the surface node). Assistant-text restore replays carry no
|
|
15
|
+
* actions and are excluded.
|
|
16
|
+
*/
|
|
17
|
+
function isEditableTarget(event) {
|
|
18
|
+
if (event === undefined || event.type !== 'user/message')
|
|
19
|
+
return false;
|
|
20
|
+
if (isAppendSurfaceEvent(event)) {
|
|
21
|
+
return event.data.source.kind === 'user' || isMessageToolsRestore(event);
|
|
22
|
+
}
|
|
23
|
+
return isMessageToolsEdit(event);
|
|
24
|
+
}
|
|
25
|
+
/** The span [target, surface tail] with full provenance, or undefined when off-surface. */
|
|
26
|
+
function surfaceSpan(surfaceNodes, targetSeq) {
|
|
27
|
+
const startIndex = surfaceNodes.indexOf(targetSeq);
|
|
28
|
+
if (startIndex === -1)
|
|
29
|
+
return undefined;
|
|
30
|
+
const sourceEventSeqs = surfaceNodes.slice(startIndex);
|
|
31
|
+
const end = sourceEventSeqs[sourceEventSeqs.length - 1];
|
|
32
|
+
/* v8 ignore next -- the slice is non-empty by construction (startIndex names a member) */
|
|
33
|
+
if (end === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { start: targetSeq, end, sourceEventSeqs };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Plan a withdrawal: the target must be an editable user message (original,
|
|
39
|
+
* restore replay, or edit replacement — see `isEditableTarget`) still present
|
|
40
|
+
* on the live surface; the span covers it and every surface node after it.
|
|
41
|
+
* @param events - the full session log (seq === index).
|
|
42
|
+
* @param surfaceNodes - the current model-visible surface seqs, in order.
|
|
43
|
+
* @param targetSeq - seq of the user message to withdraw.
|
|
44
|
+
* @returns the replacement intent, or the failure code.
|
|
45
|
+
*/
|
|
46
|
+
export function planWithdrawal(events, surfaceNodes, targetSeq) {
|
|
47
|
+
const target = events[targetSeq];
|
|
48
|
+
if (target === undefined)
|
|
49
|
+
return { ok: false, code: 'target-not-found' };
|
|
50
|
+
if (!isEditableTarget(target)) {
|
|
51
|
+
return { ok: false, code: 'not-a-user-message' };
|
|
52
|
+
}
|
|
53
|
+
const span = surfaceSpan(surfaceNodes, targetSeq);
|
|
54
|
+
if (span === undefined)
|
|
55
|
+
return { ok: false, code: 'already-withdrawn' };
|
|
56
|
+
return { ok: true, plan: span };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Plan an edit: the target must be an editable user message still present on
|
|
60
|
+
* the live surface, and the new text must be non-blank. The span covers the
|
|
61
|
+
* target and every surface node after it — editing an old message discards
|
|
62
|
+
* what followed it. The replacement's content is the new text (no
|
|
63
|
+
* placeholder), so the model context reads the edited message in place.
|
|
64
|
+
* @param events - the full session log (seq === index).
|
|
65
|
+
* @param surfaceNodes - the current model-visible surface seqs, in order.
|
|
66
|
+
* @param targetSeq - seq of the user message to edit.
|
|
67
|
+
* @param text - the edited text.
|
|
68
|
+
* @returns the replacement intent, or the failure code.
|
|
69
|
+
*/
|
|
70
|
+
export function planEdit(events, surfaceNodes, targetSeq, text) {
|
|
71
|
+
if (text.trim() === '')
|
|
72
|
+
return { ok: false, code: 'empty-text' };
|
|
73
|
+
const target = events[targetSeq];
|
|
74
|
+
if (target === undefined)
|
|
75
|
+
return { ok: false, code: 'target-not-found' };
|
|
76
|
+
if (!isEditableTarget(target)) {
|
|
77
|
+
return { ok: false, code: 'not-a-user-message' };
|
|
78
|
+
}
|
|
79
|
+
const span = surfaceSpan(surfaceNodes, targetSeq);
|
|
80
|
+
if (span === undefined)
|
|
81
|
+
return { ok: false, code: 'already-withdrawn' };
|
|
82
|
+
return { ok: true, plan: span };
|
|
83
|
+
}
|
|
84
|
+
/** Join the text blocks of one message's content into one plain string. */
|
|
85
|
+
function joinText(blocks) {
|
|
86
|
+
return blocks
|
|
87
|
+
.filter((block) => block.type === 'text'
|
|
88
|
+
&& typeof block.text === 'string')
|
|
89
|
+
.map(block => block.text)
|
|
90
|
+
.join('');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Join an assistant message's visible text. Normal replies use text blocks;
|
|
94
|
+
* interrupted replies may contain only reasoning. Prefer text when present,
|
|
95
|
+
* otherwise preserve the reasoning so a restore does not drop the only
|
|
96
|
+
* assistant content the user saw.
|
|
97
|
+
*/
|
|
98
|
+
function joinAssistantMessageText(blocks) {
|
|
99
|
+
const text = joinText(blocks);
|
|
100
|
+
if (text !== '')
|
|
101
|
+
return text;
|
|
102
|
+
return blocks
|
|
103
|
+
.filter((block) => block.type === 'reasoning'
|
|
104
|
+
&& typeof block.text === 'string')
|
|
105
|
+
.map(block => block.text)
|
|
106
|
+
.join('');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The full log seqs shadowed by the latest message-tools withdrawal
|
|
110
|
+
* replacement citing `targetSeq`, or undefined when no such replacement exists
|
|
111
|
+
* (the message left the surface through another producer, e.g. compaction).
|
|
112
|
+
*
|
|
113
|
+
* `sourceEventSeqs` is authoritative for the shadowed *surface* nodes, but an
|
|
114
|
+
* interrupted assistant step may exist only as `assistant/chunk` events (no
|
|
115
|
+
* `assistant/message` ever landed). Those chunks are not surface nodes, so
|
|
116
|
+
* they are absent from `sourceEventSeqs`. The replacement's own `start` and
|
|
117
|
+
* `seq` delimit the whole withdrawn log interval — `[start, seq)` — and are
|
|
118
|
+
* still read from the replacement itself, never guessed from another producer.
|
|
119
|
+
*/
|
|
120
|
+
function findWithdrawnSpan(events, targetSeq) {
|
|
121
|
+
for (let index = events.length - 1; index > targetSeq; index--) {
|
|
122
|
+
const event = events[index];
|
|
123
|
+
if (event !== undefined && isMessageToolsReplacement(event) && event.sourceEventSeqs?.includes(targetSeq)) {
|
|
124
|
+
const span = [];
|
|
125
|
+
for (let seq = event.surfaceOp.start; seq < event.seq; seq++)
|
|
126
|
+
span.push(seq);
|
|
127
|
+
return span;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Fold one withdrawn span into its replay entries, in span (original) order.
|
|
134
|
+
* Replayed: user-source messages, edit replacements (their content is the
|
|
135
|
+
* last edit's new text), earlier restore replays, and assistant text (framed;
|
|
136
|
+
* restore-assistant replays already carry the frame). Skipped: withdrawal
|
|
137
|
+
* placeholders, edit triggers, other plugin context, and tool calls/results.
|
|
138
|
+
*
|
|
139
|
+
* Assistant text normally comes from `assistant/message` surface events. A
|
|
140
|
+
* turn that was interrupted before finalization may never emit
|
|
141
|
+
* `assistant/message`; its visible content survives only as `assistant/chunk`
|
|
142
|
+
* rows. Those chunks are folded into one assistant replay per step when no
|
|
143
|
+
* final `assistant/message` exists for that step, so withdrawing/restoring an
|
|
144
|
+
* interrupted reply does not lose the partial assistant content.
|
|
145
|
+
*/
|
|
146
|
+
function replayEntries(events, spanSeqs) {
|
|
147
|
+
const entries = [];
|
|
148
|
+
let pending;
|
|
149
|
+
const finalizedSteps = new Set();
|
|
150
|
+
const flushPending = () => {
|
|
151
|
+
if (pending === undefined)
|
|
152
|
+
return;
|
|
153
|
+
if (!pending.finalized) {
|
|
154
|
+
const text = [...pending.text.values()].join('');
|
|
155
|
+
const reasoning = [...pending.reasoning.values()].join('');
|
|
156
|
+
const replay = text !== '' ? text : reasoning;
|
|
157
|
+
if (replay !== '') {
|
|
158
|
+
entries.push({
|
|
159
|
+
role: 'assistant',
|
|
160
|
+
text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
|
|
161
|
+
sourceSeq: pending.sourceSeq,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
pending = undefined;
|
|
166
|
+
};
|
|
167
|
+
const pushChunk = (event) => {
|
|
168
|
+
if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`))
|
|
169
|
+
return;
|
|
170
|
+
if (pending === undefined
|
|
171
|
+
|| pending.turn !== event.data.turn
|
|
172
|
+
|| pending.step !== event.data.step) {
|
|
173
|
+
flushPending();
|
|
174
|
+
pending = {
|
|
175
|
+
turn: event.data.turn,
|
|
176
|
+
step: event.data.step,
|
|
177
|
+
sourceSeq: event.seq,
|
|
178
|
+
text: new Map(),
|
|
179
|
+
reasoning: new Map(),
|
|
180
|
+
finalized: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const chunk = event.data.chunk;
|
|
184
|
+
if (chunk.type === 'text-delta') {
|
|
185
|
+
pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? '') + chunk.text);
|
|
186
|
+
}
|
|
187
|
+
else if (chunk.type === 'block-end' && chunk.block.type === 'text') {
|
|
188
|
+
pending.text.set(chunk.index, chunk.block.text);
|
|
189
|
+
}
|
|
190
|
+
else if (chunk.type === 'reasoning-delta') {
|
|
191
|
+
pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? '') + chunk.text);
|
|
192
|
+
}
|
|
193
|
+
else if (chunk.type === 'block-end' && chunk.block.type === 'reasoning') {
|
|
194
|
+
pending.reasoning.set(chunk.index, chunk.block.text);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const pushAssistantText = (text, sourceSeq) => {
|
|
198
|
+
if (text !== '') {
|
|
199
|
+
entries.push({ role: 'assistant', text: `${RESTORED_ASSISTANT_NOTICE}\n${text}`, sourceSeq });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
for (const seq of spanSeqs) {
|
|
203
|
+
const event = events[seq];
|
|
204
|
+
if (event === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
if (isMessageToolsEdit(event)) {
|
|
207
|
+
flushPending();
|
|
208
|
+
entries.push({ role: 'user', content: event.data.content, sourceSeq: seq });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (event.type === 'assistant/chunk') {
|
|
212
|
+
pushChunk(event);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (event.type === 'user/message') {
|
|
216
|
+
flushPending();
|
|
217
|
+
if (event.surfaceOp !== 'append')
|
|
218
|
+
continue;
|
|
219
|
+
if (event.data.source.kind === 'user' || isMessageToolsRestore(event)) {
|
|
220
|
+
entries.push({ role: 'user', content: event.data.content, sourceSeq: seq });
|
|
221
|
+
}
|
|
222
|
+
else if (isMessageToolsRestoreAssistant(event)) {
|
|
223
|
+
// Restore-assistant replays already carry the model-facing frame; do
|
|
224
|
+
// not add a second one.
|
|
225
|
+
entries.push({ role: 'assistant', text: joinText(event.data.content), sourceSeq: seq });
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (event.type === 'assistant/message') {
|
|
230
|
+
// A finalized step supersedes any pending chunk accumulation for the
|
|
231
|
+
// same step: flush without emitting the partial, then replay the final.
|
|
232
|
+
finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
|
|
233
|
+
if (pending !== undefined
|
|
234
|
+
&& pending.turn === event.data.turn
|
|
235
|
+
&& pending.step === event.data.step) {
|
|
236
|
+
pending.finalized = true;
|
|
237
|
+
}
|
|
238
|
+
flushPending();
|
|
239
|
+
pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Log-only events (boundaries, headers, titles) do not split a chunk run;
|
|
243
|
+
// the pending assistant step is flushed at the next replayable boundary or
|
|
244
|
+
// when a different step starts.
|
|
245
|
+
}
|
|
246
|
+
flushPending();
|
|
247
|
+
return entries;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Plan a restore: the target must be an editable user message (the same
|
|
251
|
+
* vocabulary as withdrawal — original, restore replay, or edit replacement)
|
|
252
|
+
* that is no longer on the live surface (i.e. withdrawn). Restoring an edit
|
|
253
|
+
* replacement replays the span's last edit text — the replacement's content
|
|
254
|
+
* IS the new text. The surface model is positional — a replaced span folds
|
|
255
|
+
* into exactly one node — so the withdrawn span can never be put back in
|
|
256
|
+
* place; restore replays the span's replayable content (user messages, the
|
|
257
|
+
* last edit's new text, assistant text) as fresh tail messages in original
|
|
258
|
+
* order. When no message-tools replacement cites the target (a foreign
|
|
259
|
+
* shadowing, e.g. compaction), only the target message replays — the span
|
|
260
|
+
* boundary is never re-guessed.
|
|
261
|
+
* @param events - the full session log (seq === index).
|
|
262
|
+
* @param surfaceNodes - the current model-visible surface seqs.
|
|
263
|
+
* @param targetSeq - seq of the withdrawn user message to restore.
|
|
264
|
+
* @returns the restore intent, or the failure code.
|
|
265
|
+
*/
|
|
266
|
+
export function planRestore(events, surfaceNodes, targetSeq) {
|
|
267
|
+
const target = events[targetSeq];
|
|
268
|
+
if (target === undefined)
|
|
269
|
+
return { ok: false, code: 'target-not-found' };
|
|
270
|
+
if (!isEditableTarget(target))
|
|
271
|
+
return { ok: false, code: 'not-a-user-message' };
|
|
272
|
+
if (surfaceNodes.includes(targetSeq))
|
|
273
|
+
return { ok: false, code: 'not-withdrawn' };
|
|
274
|
+
const span = findWithdrawnSpan(events, targetSeq);
|
|
275
|
+
const entries = span === undefined
|
|
276
|
+
? [{ role: 'user', content: target.data.content, sourceSeq: targetSeq }]
|
|
277
|
+
: replayEntries(events, span);
|
|
278
|
+
return { ok: true, plan: { entries } };
|
|
279
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khorsheed/dsh-client-message-tools",
|
|
3
|
+
"description": "User-message edit (withdraw and resend) and withdrawal: the withdrawal writes a surface replacement that hides the message and everything after it from the model, with a chat divider marking the spot",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"dsh",
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"cordis",
|
|
8
|
+
"messages"
|
|
9
|
+
],
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "lib/index.js",
|
|
13
|
+
"types": "lib/types/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./lib/types/index.d.ts",
|
|
17
|
+
"default": "./lib/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./invariant": {
|
|
20
|
+
"types": "./lib/types/invariant.d.ts",
|
|
21
|
+
"default": "./lib/invariant.js"
|
|
22
|
+
},
|
|
23
|
+
"./client": {
|
|
24
|
+
"types": "./lib/types/client/index.d.ts",
|
|
25
|
+
"default": "./lib/client.js"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./lib/types/types.d.ts",
|
|
29
|
+
"default": "./lib/types/types.js"
|
|
30
|
+
},
|
|
31
|
+
"./typert": {
|
|
32
|
+
"types": "./lib/typert.host.d.ts",
|
|
33
|
+
"default": "./lib/typert.host.js"
|
|
34
|
+
},
|
|
35
|
+
"./remote": {
|
|
36
|
+
"types": "./lib/typert.remote-client.d.ts",
|
|
37
|
+
"default": "./lib/typert.remote-client.js"
|
|
38
|
+
},
|
|
39
|
+
"./src/*": "./src/*",
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"dsh": {
|
|
43
|
+
"bundle": {
|
|
44
|
+
"patch": "./cordis.patch.yml"
|
|
45
|
+
},
|
|
46
|
+
"client": {
|
|
47
|
+
"inject": [
|
|
48
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
49
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
50
|
+
"@deepseek-ai/dsh-client-locale",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-model-selection"
|
|
53
|
+
],
|
|
54
|
+
"platform": "web"
|
|
55
|
+
},
|
|
56
|
+
"compat": {
|
|
57
|
+
"minHost": "0.1.0-rc.8",
|
|
58
|
+
"verifiedHost": "0.1.1-rc.2"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
66
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.0-rc.6",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
71
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
72
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
73
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
74
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
|
|
75
|
+
"react": "^18.2.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
79
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.1.1-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.1-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.1",
|
|
92
|
+
"@testing-library/react": "^16.1.0",
|
|
93
|
+
"@types/node": "^26.2.0",
|
|
94
|
+
"@types/react": "~18.3.1",
|
|
95
|
+
"react": "^18.2.0",
|
|
96
|
+
"react-dom": "^18.2.0"
|
|
97
|
+
},
|
|
98
|
+
"files": [
|
|
99
|
+
"lib",
|
|
100
|
+
"cordis.patch.yml",
|
|
101
|
+
"lib/typert.host.d.ts",
|
|
102
|
+
"lib/typert.host.js",
|
|
103
|
+
"lib/typert.remote-client.d.ts",
|
|
104
|
+
"lib/typert.remote-client.js",
|
|
105
|
+
"LICENSE",
|
|
106
|
+
"CHANGELOG.md"
|
|
107
|
+
]
|
|
108
|
+
}
|