dsh-client-auto-continue 0.5.2 → 0.5.4
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 +1 -0
- package/README.zh.md +1 -0
- package/package.json +7 -3
- package/src/client/engine.ts +1116 -0
- package/src/client/index.ts +91 -0
- package/src/client/locales.ts +140 -0
- package/src/client/settings-card.tsx +547 -0
- package/src/client/settings-form.ts +293 -0
- package/src/client/styles.ts +172 -0
- package/src/index.ts +62 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staged form model behind the plugin settings card — a self-contained
|
|
3
|
+
* implementation of the plugin-card store pattern used by the DSH plugin
|
|
4
|
+
* configuration section.
|
|
5
|
+
*
|
|
6
|
+
* A card stages what the user types and writes it only when they save. Each
|
|
7
|
+
* settings write is a durable, revision-fenced document mutation, so staging
|
|
8
|
+
* keeps what is on screen exactly what a save would store. A field shows its
|
|
9
|
+
* effective value — the user layer over the composition layer over the schema
|
|
10
|
+
* default — and whether the user layer carries it (presence, not value
|
|
11
|
+
* equality, marks an override).
|
|
12
|
+
*/
|
|
13
|
+
import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
14
|
+
|
|
15
|
+
/** The write one field's staged text performs when the card is saved. */
|
|
16
|
+
export type FieldWrite = { kind: 'set'; value: unknown } | { kind: 'clear' };
|
|
17
|
+
|
|
18
|
+
/** How one field converts between its stored value and its draft text. */
|
|
19
|
+
export interface CardFieldSpec {
|
|
20
|
+
/** Field name inside the namespace section. */
|
|
21
|
+
field: string;
|
|
22
|
+
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
23
|
+
format: (value: unknown) => string;
|
|
24
|
+
/**
|
|
25
|
+
* The write this draft text stages, or undefined when the text is not a
|
|
26
|
+
* value this field accepts — which blocks the save rather than discarding it.
|
|
27
|
+
*/
|
|
28
|
+
parse: (text: string) => FieldWrite | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One field as the card's control renders it. */
|
|
32
|
+
export interface CardFieldState {
|
|
33
|
+
/** Draft text the control renders. */
|
|
34
|
+
text: string;
|
|
35
|
+
/** Whether saving would leave a user-layer entry for this field. */
|
|
36
|
+
overridden: boolean;
|
|
37
|
+
/** Whether the draft is not a value this field accepts, which blocks saving. */
|
|
38
|
+
invalid: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Form state every plugin card shares. */
|
|
42
|
+
export interface CardShell {
|
|
43
|
+
/** False while the namespace is not served to this client; the card renders nothing. */
|
|
44
|
+
available: boolean;
|
|
45
|
+
/** Whether the Host document accepts writes. */
|
|
46
|
+
writable: boolean;
|
|
47
|
+
/** Whether the form holds edits that a save would write. */
|
|
48
|
+
dirty: boolean;
|
|
49
|
+
/** Whether any staged draft is invalid, which blocks the save. */
|
|
50
|
+
invalid: boolean;
|
|
51
|
+
/** Whether a save is crossing the wire. */
|
|
52
|
+
saving: boolean;
|
|
53
|
+
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
54
|
+
failed: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The write actions the card's slot entry injects. */
|
|
58
|
+
export interface CardActions {
|
|
59
|
+
/** Stage draft text for one field. */
|
|
60
|
+
edit: (field: string, text: string) => void;
|
|
61
|
+
/** Stage a clear, so saving lets the field re-inherit the composition layer. */
|
|
62
|
+
resetField: (field: string) => void;
|
|
63
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
64
|
+
save: () => void;
|
|
65
|
+
/** Drop every staged edit. */
|
|
66
|
+
discard: () => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A whole-number field. An empty draft clears the field; a non-number or out-of-range draft blocks the save. */
|
|
70
|
+
export function numberField(field: string, min = 0): CardFieldSpec {
|
|
71
|
+
return {
|
|
72
|
+
field,
|
|
73
|
+
format: (value) => (typeof value === 'number' ? String(value) : ''),
|
|
74
|
+
parse: (text) => {
|
|
75
|
+
const trimmed = text.trim();
|
|
76
|
+
if (trimmed === '') return { kind: 'clear' };
|
|
77
|
+
const parsed = Number(trimmed);
|
|
78
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < min) return undefined;
|
|
79
|
+
return { kind: 'set', value: parsed };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A free-text field. An empty draft clears the field, so emptying the control and saving is the same gesture as resetting it. */
|
|
85
|
+
export function textField(field: string): CardFieldSpec {
|
|
86
|
+
return {
|
|
87
|
+
field,
|
|
88
|
+
format: (value) => (typeof value === 'string' ? value : ''),
|
|
89
|
+
parse: (text) => {
|
|
90
|
+
const trimmed = text.trim();
|
|
91
|
+
return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A boolean field, edited through true/false draft text; an empty draft inherits. */
|
|
97
|
+
export function booleanField(field: string): CardFieldSpec {
|
|
98
|
+
return {
|
|
99
|
+
field,
|
|
100
|
+
format: (value) => (typeof value === 'boolean' ? String(value) : ''),
|
|
101
|
+
parse: (text) => {
|
|
102
|
+
const trimmed = text.trim();
|
|
103
|
+
if (trimmed === '') return { kind: 'clear' };
|
|
104
|
+
if (trimmed === 'true') return { kind: 'set', value: true };
|
|
105
|
+
if (trimmed === 'false') return { kind: 'set', value: false };
|
|
106
|
+
return undefined;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One field's staged edit. */
|
|
112
|
+
interface StagedEdit {
|
|
113
|
+
/** Draft text the control renders. */
|
|
114
|
+
text: string;
|
|
115
|
+
/** True when this edit clears the field whatever text it shows. */
|
|
116
|
+
clear: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Stages one card's edits over one settings namespace and writes them on save.
|
|
121
|
+
*
|
|
122
|
+
* The Host is the only authority on whether a value was accepted, so the
|
|
123
|
+
* outcome is read back from the section rather than predicted here. A save
|
|
124
|
+
* that did not land keeps its drafts, so the user can correct them instead of
|
|
125
|
+
* retyping.
|
|
126
|
+
*/
|
|
127
|
+
export class CardForm<T> {
|
|
128
|
+
private readonly specs: Map<string, CardFieldSpec>;
|
|
129
|
+
private readonly staged = new Map<string, StagedEdit>();
|
|
130
|
+
private readonly listeners = new Set<() => void>();
|
|
131
|
+
private saving = false;
|
|
132
|
+
private failed = false;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param scope - the bound settings scope for this card's namespace.
|
|
136
|
+
* @param specs - the section fields this card edits.
|
|
137
|
+
*/
|
|
138
|
+
constructor(
|
|
139
|
+
private readonly scope: SettingsScope<T>,
|
|
140
|
+
specs: CardFieldSpec[],
|
|
141
|
+
) {
|
|
142
|
+
this.specs = new Map(specs.map((spec) => [spec.field, spec]));
|
|
143
|
+
this.scope.subscribe(() => this.publish());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
147
|
+
bind<S>(project: () => S, createStore: (init: S) => SnapshotStore<S>): SnapshotStore<S> {
|
|
148
|
+
const store = createStore(project());
|
|
149
|
+
this.listeners.add(() => store.set(project()));
|
|
150
|
+
return store;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Read the card-level state: what the Host serves, and what a save would do. */
|
|
154
|
+
shell(): CardShell {
|
|
155
|
+
const snapshot = this.scope.getSnapshot();
|
|
156
|
+
return {
|
|
157
|
+
available: snapshot.status === 'ready',
|
|
158
|
+
writable: snapshot.writable,
|
|
159
|
+
dirty: this.plan().length > 0,
|
|
160
|
+
invalid: this.plan().some((item) => item.run === undefined),
|
|
161
|
+
saving: this.saving,
|
|
162
|
+
failed: this.failed,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Read one field's state from the effective section and its staged draft. */
|
|
167
|
+
field(field: string): CardFieldState {
|
|
168
|
+
const spec = this.specOf(field);
|
|
169
|
+
const staged = this.staged.get(field);
|
|
170
|
+
if (staged === undefined) {
|
|
171
|
+
return {
|
|
172
|
+
text: spec.format(this.sectionValue(field)),
|
|
173
|
+
overridden: this.stored(field),
|
|
174
|
+
invalid: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const write = staged.clear ? { kind: 'clear' as const } : spec.parse(staged.text);
|
|
178
|
+
return {
|
|
179
|
+
text: staged.text,
|
|
180
|
+
overridden: write?.kind === 'set',
|
|
181
|
+
invalid: write === undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The actions the card's slot registration injects. */
|
|
186
|
+
actions(): CardActions {
|
|
187
|
+
return {
|
|
188
|
+
edit: (field, text) => this.stage(field, { text, clear: false }),
|
|
189
|
+
resetField: (field) => {
|
|
190
|
+
this.stage(field, { text: this.specOf(field).format(this.baseValue(field)), clear: true });
|
|
191
|
+
},
|
|
192
|
+
save: () => void this.save(),
|
|
193
|
+
discard: () => {
|
|
194
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
195
|
+
this.staged.clear();
|
|
196
|
+
this.failed = false;
|
|
197
|
+
this.publish();
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Write every staged edit, then re-seed from what the Host accepted.
|
|
204
|
+
* @returns settlement after every write and the read-back.
|
|
205
|
+
*/
|
|
206
|
+
async save(): Promise<void> {
|
|
207
|
+
const plan = this.plan();
|
|
208
|
+
const writes = plan.flatMap((item) => (item.run === undefined ? [] : [item.run]));
|
|
209
|
+
if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
|
|
210
|
+
// Snapshot the fields this save writes, so edits staged while it is in
|
|
211
|
+
// flight survive: only the staged keys this save actually wrote are cleared.
|
|
212
|
+
const fields = new Set(plan.map((item) => item.field));
|
|
213
|
+
this.saving = true;
|
|
214
|
+
this.failed = false;
|
|
215
|
+
this.publish();
|
|
216
|
+
let landed = true;
|
|
217
|
+
for (const write of writes) {
|
|
218
|
+
landed = (await write()) && landed;
|
|
219
|
+
}
|
|
220
|
+
if (landed) {
|
|
221
|
+
for (const field of fields) this.staged.delete(field);
|
|
222
|
+
}
|
|
223
|
+
this.saving = false;
|
|
224
|
+
this.failed = !landed;
|
|
225
|
+
this.publish();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Every staged edit a save would write. An entry whose draft is not a value
|
|
230
|
+
* its field accepts carries no write: the form is still dirty, and the save
|
|
231
|
+
* refuses rather than dropping the edit. A staged edit that matches the
|
|
232
|
+
* effective section is not a write at all.
|
|
233
|
+
*/
|
|
234
|
+
private plan(): { field: string; run: (() => Promise<boolean>) | undefined }[] {
|
|
235
|
+
const plan: { field: string; run: (() => Promise<boolean>) | undefined }[] = [];
|
|
236
|
+
for (const [field, staged] of this.staged) {
|
|
237
|
+
const spec = this.specOf(field);
|
|
238
|
+
if (staged.clear) {
|
|
239
|
+
if (this.stored(field)) plan.push({ field, run: () => this.clear(field) });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
243
|
+
const write = spec.parse(staged.text);
|
|
244
|
+
if (write === undefined) plan.push({ field, run: undefined });
|
|
245
|
+
else if (write.kind === 'clear') plan.push({ field, run: () => this.clear(field) });
|
|
246
|
+
else plan.push({ field, run: () => this.store(field, write.value) });
|
|
247
|
+
}
|
|
248
|
+
return plan;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async clear(field: string): Promise<boolean> {
|
|
252
|
+
await this.scope.unset(field);
|
|
253
|
+
return !this.stored(field);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async store(field: string, value: unknown): Promise<boolean> {
|
|
257
|
+
await this.scope.set(field, value);
|
|
258
|
+
return this.userLayer()?.[field] === value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private stage(field: string, edit: StagedEdit): void {
|
|
262
|
+
this.staged.set(field, edit);
|
|
263
|
+
this.failed = false;
|
|
264
|
+
this.publish();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private specOf(field: string): CardFieldSpec {
|
|
268
|
+
const spec = this.specs.get(field);
|
|
269
|
+
if (spec === undefined) throw new Error(`settings card has no field ${field}`);
|
|
270
|
+
return spec;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private sectionValue(field: string): unknown {
|
|
274
|
+
return (this.scope.getSnapshot().value as Record<string, unknown> | undefined)?.[field];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private baseValue(field: string): unknown {
|
|
278
|
+
return (this.scope.getSnapshot().base as Record<string, unknown> | undefined)?.[field];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private userLayer(): Record<string, unknown> | undefined {
|
|
282
|
+
return this.scope.getSnapshot().user as Record<string, unknown> | undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private stored(field: string): boolean {
|
|
286
|
+
const user = this.userLayer();
|
|
287
|
+
return user !== undefined && Object.prototype.hasOwnProperty.call(user, field);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private publish(): void {
|
|
291
|
+
for (const listener of this.listeners) listener();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles for the auto-continue settings card, injected at factory
|
|
3
|
+
* materialization so the client module system's style bookkeeping (HMR) owns
|
|
4
|
+
* them. Uses the DSH design tokens (`--dsw-alias-*`) so the card follows the
|
|
5
|
+
* active theme.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const css = `
|
|
9
|
+
.dshAcCard {
|
|
10
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
11
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
list-style: none;
|
|
14
|
+
transition: border-color .16s, background .16s;
|
|
15
|
+
}
|
|
16
|
+
.dshAcCard:hover { border-color: var(--dsw-alias-label-dimmed); }
|
|
17
|
+
.dshAcCardOpen {
|
|
18
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
19
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
20
|
+
}
|
|
21
|
+
.dshAcHeader {
|
|
22
|
+
appearance: none;
|
|
23
|
+
width: 100%;
|
|
24
|
+
font: inherit;
|
|
25
|
+
color: inherit;
|
|
26
|
+
text-align: left;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
background: none;
|
|
29
|
+
border: 0;
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
padding: 14px 16px;
|
|
34
|
+
display: flex;
|
|
35
|
+
}
|
|
36
|
+
.dshAcHeader:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
|
|
37
|
+
.dshAcHeadText { flex-direction: column; flex: 1; gap: 4px; min-width: 0; display: flex; }
|
|
38
|
+
.dshAcName { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
|
|
39
|
+
.dshAcDescription { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }
|
|
40
|
+
.dshAcChevron { color: var(--dsw-alias-label-tertiary); flex: none; transition: transform .16s; }
|
|
41
|
+
.dshAcChevronOpen { transform: rotate(180deg); }
|
|
42
|
+
.dshAcBody { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding-bottom: 8px; }
|
|
43
|
+
.dshAcReadOnly { color: var(--dsw-alias-label-tertiary); margin: 12px 0 0; font-size: 12px; line-height: 1.5; }
|
|
44
|
+
.dshAcPending {
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
47
|
+
color: var(--dsw-alias-label-secondary);
|
|
48
|
+
border-radius: 999px;
|
|
49
|
+
flex: none;
|
|
50
|
+
padding: 1px 8px;
|
|
51
|
+
font-size: 11px;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
line-height: 17px;
|
|
54
|
+
}
|
|
55
|
+
.dshAcFooter {
|
|
56
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
57
|
+
justify-content: flex-end;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 8px;
|
|
60
|
+
padding: 12px 0 4px;
|
|
61
|
+
display: flex;
|
|
62
|
+
}
|
|
63
|
+
.dshAcFailed { min-width: 0; color: var(--dsw-alias-label-error); flex: 1; margin: 0; font-size: 12px; line-height: 1.5; }
|
|
64
|
+
.dshAcDiscard, .dshAcSave {
|
|
65
|
+
appearance: none;
|
|
66
|
+
font: inherit;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
border: 1px solid transparent;
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
padding: 5px 14px;
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
}
|
|
74
|
+
.dshAcDiscard { border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); background: none; }
|
|
75
|
+
.dshAcDiscard:hover:not(:disabled) { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
76
|
+
.dshAcSave { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); }
|
|
77
|
+
.dshAcDiscard:disabled, .dshAcSave:disabled { opacity: .4; cursor: default; }
|
|
78
|
+
.dshAcDiscard:focus-visible, .dshAcSave:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
|
|
79
|
+
.dshAcField { flex-direction: column; gap: 6px; padding: 12px 0; display: flex; }
|
|
80
|
+
.dshAcField + .dshAcField { border-top: 1px solid var(--dsw-alias-border-l2); }
|
|
81
|
+
.dshAcHead { align-items: center; gap: 8px; display: flex; }
|
|
82
|
+
.dshAcLabel { min-width: 0; color: var(--dsw-alias-label-primary); flex: 1; font-size: 13px; font-weight: 500; line-height: 1.5; }
|
|
83
|
+
.dshAcBadges { align-items: center; gap: 8px; display: inline-flex; }
|
|
84
|
+
.dshAcBadge {
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
87
|
+
color: var(--dsw-alias-label-secondary);
|
|
88
|
+
border-radius: 999px;
|
|
89
|
+
padding: 1px 8px;
|
|
90
|
+
font-size: 11px;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
line-height: 17px;
|
|
93
|
+
}
|
|
94
|
+
.dshAcReset {
|
|
95
|
+
font: inherit;
|
|
96
|
+
color: var(--dsw-alias-label-secondary);
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
background: none;
|
|
99
|
+
border: none;
|
|
100
|
+
padding: 0;
|
|
101
|
+
font-size: 12px;
|
|
102
|
+
line-height: 1.5;
|
|
103
|
+
}
|
|
104
|
+
.dshAcReset:hover:not(:disabled) { color: var(--dsw-alias-label-primary); }
|
|
105
|
+
.dshAcReset:disabled { cursor: default; }
|
|
106
|
+
.dshAcInput {
|
|
107
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
108
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
109
|
+
height: 34px;
|
|
110
|
+
font: inherit;
|
|
111
|
+
color: var(--dsw-alias-label-primary);
|
|
112
|
+
border-radius: 8px;
|
|
113
|
+
padding: 0 12px;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
line-height: 1.5;
|
|
116
|
+
}
|
|
117
|
+
.dshAcInput:focus-visible { border-color: var(--dsw-alias-brand-primary); outline: none; }
|
|
118
|
+
.dshAcInput:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
119
|
+
.dshAcInputInvalid { border-color: var(--dsw-alias-label-error); }
|
|
120
|
+
.dshAcSelect {
|
|
121
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
122
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
123
|
+
height: 34px;
|
|
124
|
+
font: inherit;
|
|
125
|
+
color: var(--dsw-alias-label-primary);
|
|
126
|
+
border-radius: 8px;
|
|
127
|
+
padding: 0 8px;
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
line-height: 1.5;
|
|
130
|
+
}
|
|
131
|
+
.dshAcSelect:focus-visible { border-color: var(--dsw-alias-brand-primary); outline: none; }
|
|
132
|
+
.dshAcSelect:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
133
|
+
.dshAcInvalid { color: var(--dsw-alias-label-error); margin: 0; font-size: 12px; line-height: 1.5; }
|
|
134
|
+
.dshAcHint { color: var(--dsw-alias-label-tertiary); margin: 0; font-size: 12px; line-height: 1.5; }
|
|
135
|
+
.dshAcPanel { border-top: 1px solid var(--dsw-alias-border-l2); flex-direction: column; gap: 8px; padding: 12px 0; display: flex; }
|
|
136
|
+
.dshAcPanelHead { align-items: center; gap: 8px; display: flex; }
|
|
137
|
+
.dshAcPanelTitle { color: var(--dsw-alias-label-primary); flex: 1; font-size: 13px; font-weight: 600; line-height: 1.5; }
|
|
138
|
+
.dshAcStats { gap: 4px 16px; margin: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); display: grid; }
|
|
139
|
+
.dshAcStats > div { justify-content: space-between; gap: 8px; display: flex; }
|
|
140
|
+
.dshAcStats dt { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.5; }
|
|
141
|
+
.dshAcStats dd { color: var(--dsw-alias-label-primary); margin: 0; font-size: 12px; font-weight: 600; line-height: 1.5; }
|
|
142
|
+
.dshAcCodes { flex-wrap: wrap; align-items: center; gap: 6px; display: flex; }
|
|
143
|
+
.dshAcCode {
|
|
144
|
+
white-space: nowrap;
|
|
145
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
146
|
+
color: var(--dsw-alias-label-secondary);
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
padding: 1px 8px;
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
font-weight: 500;
|
|
151
|
+
line-height: 17px;
|
|
152
|
+
}
|
|
153
|
+
.dshAcPauseList { flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; display: flex; }
|
|
154
|
+
.dshAcPauseList li { align-items: center; gap: 8px; display: flex; }
|
|
155
|
+
.dshAcPauseId {
|
|
156
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
157
|
+
color: var(--dsw-alias-label-primary);
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
line-height: 1.5;
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
/** Inject the stylesheet once; a no-op outside a browser environment. */
|
|
164
|
+
export function injectStyles(): void {
|
|
165
|
+
if (typeof document === 'undefined') return;
|
|
166
|
+
if (document.querySelector('style[data-plugin-css="auto-continue/card"]') !== null) return;
|
|
167
|
+
const tag = document.createElement('style');
|
|
168
|
+
tag.dataset.plugin = 'dsh-client-auto-continue';
|
|
169
|
+
tag.dataset.pluginCss = 'auto-continue/card';
|
|
170
|
+
tag.textContent = css;
|
|
171
|
+
document.head.appendChild(tag);
|
|
172
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the auto-continue plugin: registers the `auto-continue`
|
|
3
|
+
* settings namespace so the browser half's settings card can edit it and the
|
|
4
|
+
* engine can read it. No other host-side behavior.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
9
|
+
// Type-only: pulls the `ctx.settings` Context augmentation from dsh-settings.
|
|
10
|
+
import type {} from '@deepseek-ai/dsh-settings';
|
|
11
|
+
|
|
12
|
+
/** Settings namespace of the auto-continue plugin (lowercase kebab-case). */
|
|
13
|
+
export const AUTO_CONTINUE_NS = 'auto-continue';
|
|
14
|
+
|
|
15
|
+
/** Wire schema of the auto-continue section; defaults are the plugin's built-in values. */
|
|
16
|
+
export const AutoContinueSchema = z.object({
|
|
17
|
+
/** Text automatically sent after an interruption. */
|
|
18
|
+
continueText: z.string().default('继续'),
|
|
19
|
+
/** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
|
|
20
|
+
continueTextMaxTokens: z.string().default('继续'),
|
|
21
|
+
/** Grace period after an interruption before auto-sending (ms). */
|
|
22
|
+
graceMs: z.natural().default(3000),
|
|
23
|
+
/** Minimum interval between two auto-continues per session (ms). */
|
|
24
|
+
cooldownMs: z.natural().default(20000),
|
|
25
|
+
/** Max consecutive auto-continues per session before stopping. */
|
|
26
|
+
maxConsecutive: z.natural().min(1).default(3),
|
|
27
|
+
/** Scan recently interrupted sessions on page load / reconnect. */
|
|
28
|
+
scanOnBoot: z.boolean().default(true),
|
|
29
|
+
/** Max sessions the scan checks (most recently updated). */
|
|
30
|
+
scanLimit: z.natural().min(1).default(8),
|
|
31
|
+
/** Scan only considers interruptions inside this window (ms). */
|
|
32
|
+
freshMs: z.natural().default(15 * 60 * 1000),
|
|
33
|
+
/** Delay before scanning after a reconnect (ms). */
|
|
34
|
+
reconnectScanDelayMs: z.natural().default(5000),
|
|
35
|
+
/** SSE reconnect backoff (ms). */
|
|
36
|
+
reconnectBackoffMs: z.natural().default(3000),
|
|
37
|
+
/** Log `[auto-continue]` lines to the browser console. */
|
|
38
|
+
verbose: z.boolean().default(true),
|
|
39
|
+
/** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
|
|
40
|
+
classify: z.boolean().default(true),
|
|
41
|
+
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
42
|
+
backoffFactor: z.natural().min(1).default(2),
|
|
43
|
+
/** Cap on the effective backoff interval (ms). */
|
|
44
|
+
backoffMaxMs: z.natural().default(300000),
|
|
45
|
+
/** Show browser notifications for auto-continue events. */
|
|
46
|
+
notify: z.boolean().default(false),
|
|
47
|
+
/** Globally pause auto-continue: no live or scan send. */
|
|
48
|
+
paused: z.boolean().default(false),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Plugin body: register the settings namespace when a settings provider is
|
|
53
|
+
* composed. Changes apply live — the browser half observes the scope.
|
|
54
|
+
* @param ctx - host plugin context.
|
|
55
|
+
*/
|
|
56
|
+
export function apply(ctx: Context): void {
|
|
57
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
58
|
+
settingsCtx.settings.register(settingsNamespace(AUTO_CONTINUE_NS), AutoContinueSchema, {
|
|
59
|
+
applies: 'live',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"lib": [
|
|
8
|
+
"ES2020",
|
|
9
|
+
"DOM",
|
|
10
|
+
"DOM.Iterable"
|
|
11
|
+
],
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"noUncheckedIndexedAccess": true,
|
|
19
|
+
"types": [
|
|
20
|
+
"react"
|
|
21
|
+
],
|
|
22
|
+
"allowImportingTsExtensions": true,
|
|
23
|
+
"rewriteRelativeImportExtensions": true
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"src/**/*.ts",
|
|
27
|
+
"src/**/*.tsx"
|
|
28
|
+
]
|
|
29
|
+
}
|