dsh-reference-anything 0.2.4 → 0.3.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/NOTICE.md +5 -34
- package/README.md +84 -43
- package/cordis.patch.yml +18 -20
- package/lib/client.js +980 -1443
- package/lib/contract.d.ts +14 -13
- package/lib/contract.js +6 -8
- package/lib/host.d.ts +12 -11
- package/lib/host.js +15 -7
- package/lib/opencli.d.ts +5 -1
- package/lib/opencli.js +26 -43
- package/lib/profile-mode.d.ts +30 -0
- package/lib/profile-mode.js +90 -0
- package/lib/reference.d.ts +3 -11
- package/lib/reference.js +14 -55
- package/lib/sources/web-chat/index.d.ts +1 -0
- package/lib/sources/web-chat/index.js +5 -0
- package/lib/store/spec.d.ts +13 -0
- package/lib/sync/index.js +4 -4
- package/lib/typert.js +2 -2
- package/lib/web.js +1 -1
- package/lib/wire.d.ts +29 -0
- package/lib/wire.js +16 -7
- package/opencli-plugin/common.js +93 -33
- package/opencli-plugin/opencli-plugin.json +1 -1
- package/opencli-plugin/package.json +1 -1
- package/package.json +21 -16
- package/lib/workspace.d.ts +0 -18
- package/lib/workspace.js +0 -81
package/lib/reference.js
CHANGED
|
@@ -8,21 +8,17 @@
|
|
|
8
8
|
* the loop durably appends, so injected material is logged by construction and
|
|
9
9
|
* a replay reconstructs the request exactly.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* is resolved through `ctx.references`.
|
|
13
|
-
*
|
|
14
|
-
* session-surface semantics — compaction folding, shadowed events, cited event
|
|
15
|
-
* seqs — that do not generalize to an outside conversation.
|
|
11
|
+
* This plugin owns only `dsh-ref:`, which names material outside the harness
|
|
12
|
+
* and is resolved through `ctx.references`. Native file and DSH-session
|
|
13
|
+
* references are owned end-to-end by their official DSH services.
|
|
16
14
|
*
|
|
17
15
|
* @module dsh-reference-anything/reference
|
|
18
16
|
*/
|
|
19
17
|
import z from '@deepseek-ai/schemastery';
|
|
20
18
|
import { boundContextSummary, createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm';
|
|
21
|
-
import { SESSION_REFERENCE_SCHEME, parseSessionReferenceText, } from '@deepseek-ai/dsh-session-reference';
|
|
22
19
|
import { ReferenceAnythingError } from "./errors.js";
|
|
23
20
|
import { renderDeferredReferences } from "./render.js";
|
|
24
21
|
import { mayContainReference, parseReferenceText } from "./uri.js";
|
|
25
|
-
import { WORKSPACE_REFERENCE_SCHEME, parseWorkspaceReferenceText, renderWorkspaceReferences } from "./workspace.js";
|
|
26
22
|
/** Cordis plugin name used by loader diagnostics. */
|
|
27
23
|
export const name = 'reference-mention';
|
|
28
24
|
/** The registry this expander reads through. */
|
|
@@ -32,7 +28,6 @@ export const MAX_REFERENCES = 3;
|
|
|
32
28
|
/** Runtime schema for {@link Config}. */
|
|
33
29
|
export const Config = z.object({
|
|
34
30
|
maxReferences: z.number().step(1).min(1).max(MAX_REFERENCES).default(MAX_REFERENCES),
|
|
35
|
-
serveSessionScheme: z.boolean().default(true),
|
|
36
31
|
});
|
|
37
32
|
/**
|
|
38
33
|
* Register the pre-step mention expander.
|
|
@@ -41,7 +36,6 @@ export const Config = z.object({
|
|
|
41
36
|
*/
|
|
42
37
|
export function apply(ctx, config = {}) {
|
|
43
38
|
const maxReferences = Math.min(config.maxReferences ?? MAX_REFERENCES, MAX_REFERENCES);
|
|
44
|
-
const serveSessionScheme = config.serveSessionScheme ?? true;
|
|
45
39
|
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
46
40
|
const decision = await next();
|
|
47
41
|
if (decision.kind === 'reject' || signal.aborted)
|
|
@@ -49,7 +43,7 @@ export function apply(ctx, config = {}) {
|
|
|
49
43
|
// The overwhelmingly common case is a prompt with no mentions at all, and
|
|
50
44
|
// this runs on every step of every turn. Screening with substring scans
|
|
51
45
|
// before any parsing keeps that case free.
|
|
52
|
-
const target = decision.messages.findIndex(candidate => candidate.source.kind === 'user' && messageText(candidate).some(
|
|
46
|
+
const target = decision.messages.findIndex(candidate => candidate.source.kind === 'user' && messageText(candidate).some(mayContainReference));
|
|
53
47
|
if (target < 0)
|
|
54
48
|
return decision;
|
|
55
49
|
const original = decision.messages[target];
|
|
@@ -57,7 +51,7 @@ export function apply(ctx, config = {}) {
|
|
|
57
51
|
return decision;
|
|
58
52
|
let parsed;
|
|
59
53
|
try {
|
|
60
|
-
parsed = parseMessage(original
|
|
54
|
+
parsed = parseMessage(original);
|
|
61
55
|
}
|
|
62
56
|
catch (error) {
|
|
63
57
|
// A malformed URI inside an explicit mention is the user asking for
|
|
@@ -65,11 +59,11 @@ export function apply(ctx, config = {}) {
|
|
|
65
59
|
// reference silently resolves to nothing.
|
|
66
60
|
return entered(decision, target, [notice(describe(error))], original);
|
|
67
61
|
}
|
|
68
|
-
if (parsed.references.length === 0
|
|
62
|
+
if (parsed.references.length === 0)
|
|
69
63
|
return decision;
|
|
70
64
|
let contexts;
|
|
71
65
|
try {
|
|
72
|
-
contexts = await resolve(ctx, agent, parsed,
|
|
66
|
+
contexts = await resolve(ctx, agent, parsed, maxReferences, signal);
|
|
73
67
|
}
|
|
74
68
|
catch (error) {
|
|
75
69
|
if (signal.aborted)
|
|
@@ -85,28 +79,13 @@ export function apply(ctx, config = {}) {
|
|
|
85
79
|
* Authorize and render the named references without reading their bodies.
|
|
86
80
|
* The agent decides whether the current request warrants `reference_read`.
|
|
87
81
|
*/
|
|
88
|
-
async function resolve(ctx, agent, parsed,
|
|
82
|
+
async function resolve(ctx, agent, parsed, maxReferences, signal) {
|
|
89
83
|
const contexts = [];
|
|
90
|
-
if (parsed.workspaceReferences.length > 0) {
|
|
91
|
-
contexts.push(createUserMessage({
|
|
92
|
-
content: [{ type: 'text', text: await renderWorkspaceReferences(agent, parsed.workspaceReferences) }],
|
|
93
|
-
source: { kind: 'plugin', plugin: name, form: 'recall' },
|
|
94
|
-
}));
|
|
95
|
-
}
|
|
96
|
-
if (parsed.sessionReferences.length > 0) {
|
|
97
|
-
const resolver = ctx.get('sessionReferenceResolver');
|
|
98
|
-
if (resolver === undefined) {
|
|
99
|
-
throw new ReferenceAnythingError('this deployment cannot reference other harness sessions: the cross-session resolver is not mounted', 'SOURCE_UNAVAILABLE');
|
|
100
|
-
}
|
|
101
|
-
const prepared = await resolver.prepare(agent, contentOf(parsed.message), [...parsed.sessionReferences], signal);
|
|
102
|
-
if (prepared.additionalContext !== undefined)
|
|
103
|
-
contexts.push(prepared.additionalContext);
|
|
104
|
-
}
|
|
105
84
|
if (parsed.references.length > 0) {
|
|
106
85
|
const inputs = dedupe(parsed.references);
|
|
107
|
-
if (inputs.length >
|
|
108
|
-
throw new ReferenceAnythingError(`a message may reference at most ${
|
|
109
|
-
+ `${
|
|
86
|
+
if (inputs.length > maxReferences) {
|
|
87
|
+
throw new ReferenceAnythingError(`a message may reference at most ${maxReferences} outside conversation`
|
|
88
|
+
+ `${maxReferences === 1 ? '' : 's'}; this one named ${inputs.length}`, 'REFERENCE_TOO_MANY');
|
|
110
89
|
}
|
|
111
90
|
for (const input of inputs)
|
|
112
91
|
ctx.references.grant(String(agent.session.id), input.ref);
|
|
@@ -134,28 +113,16 @@ function entered(decision, target, contexts, message) {
|
|
|
134
113
|
const replaced = decision.messages.with(target, message);
|
|
135
114
|
return { kind: 'enter', messages: replaced.toSpliced(target, 0, ...contexts) };
|
|
136
115
|
}
|
|
137
|
-
/** Rewrite
|
|
138
|
-
function parseMessage(message
|
|
116
|
+
/** Rewrite external-conversation mentions to readable text, preserving message identity. */
|
|
117
|
+
function parseMessage(message) {
|
|
139
118
|
const references = [];
|
|
140
|
-
const sessionReferences = [];
|
|
141
|
-
const workspaceReferences = [];
|
|
142
119
|
let changed = false;
|
|
143
120
|
const content = message.content.map((block) => {
|
|
144
121
|
if (block.type !== 'text')
|
|
145
122
|
return block;
|
|
146
123
|
const own = parseReferenceText(block.text);
|
|
147
124
|
references.push(...own.references);
|
|
148
|
-
|
|
149
|
-
if (text.includes(WORKSPACE_REFERENCE_SCHEME)) {
|
|
150
|
-
const workspace = parseWorkspaceReferenceText(text);
|
|
151
|
-
workspaceReferences.push(...workspace.references);
|
|
152
|
-
text = workspace.text;
|
|
153
|
-
}
|
|
154
|
-
if (serveSessionScheme && text.includes(SESSION_REFERENCE_SCHEME)) {
|
|
155
|
-
const session = parseSessionReferenceText(text);
|
|
156
|
-
sessionReferences.push(...session.references);
|
|
157
|
-
text = session.text;
|
|
158
|
-
}
|
|
125
|
+
const text = own.text;
|
|
159
126
|
if (text === block.text)
|
|
160
127
|
return block;
|
|
161
128
|
changed = true;
|
|
@@ -166,8 +133,6 @@ function parseMessage(message, serveSessionScheme) {
|
|
|
166
133
|
return {
|
|
167
134
|
message: changed ? freezeMessage({ ...message, content }) : message,
|
|
168
135
|
references,
|
|
169
|
-
sessionReferences,
|
|
170
|
-
workspaceReferences,
|
|
171
136
|
};
|
|
172
137
|
}
|
|
173
138
|
/** First-appearance order, one entry per distinct reference. */
|
|
@@ -183,15 +148,9 @@ function dedupe(references) {
|
|
|
183
148
|
}
|
|
184
149
|
return unique;
|
|
185
150
|
}
|
|
186
|
-
function mentions(text, serveSessionScheme) {
|
|
187
|
-
return mayContainReference(text) || text.includes(WORKSPACE_REFERENCE_SCHEME) || (serveSessionScheme && text.includes(SESSION_REFERENCE_SCHEME));
|
|
188
|
-
}
|
|
189
151
|
function messageText(message) {
|
|
190
152
|
return message.content.flatMap(block => block.type === 'text' ? [block.text] : []);
|
|
191
153
|
}
|
|
192
|
-
function contentOf(message) {
|
|
193
|
-
return message.content.map(block => ({ ...block }));
|
|
194
|
-
}
|
|
195
154
|
function notice(summary) {
|
|
196
155
|
return createUserMessage({
|
|
197
156
|
content: [{
|
|
@@ -94,6 +94,7 @@ export default class WebChatHistoryService extends Service implements ReferenceS
|
|
|
94
94
|
installOpenCli(signal?: AbortSignal): Promise<OpenCliDiscovery>;
|
|
95
95
|
updateSettings(value: unknown): Promise<SettingsRecord>;
|
|
96
96
|
health(signal?: AbortSignal): Promise<import("../../opencli.ts").OpenCliHealth>;
|
|
97
|
+
quickHealth(signal?: AbortSignal): Promise<import("../../opencli.ts").OpenCliHealth>;
|
|
97
98
|
profiles(signal?: AbortSignal): Promise<import("../../opencli.ts").BrowserProfile[]>;
|
|
98
99
|
private runner;
|
|
99
100
|
installAdapter(signal?: AbortSignal): Promise<boolean>;
|
|
@@ -229,6 +229,11 @@ export default class WebChatHistoryService extends Service {
|
|
|
229
229
|
return new OpenCliRunner({ executable: settings.opencliPath, profile: settings.profile,
|
|
230
230
|
timeoutMs: this.config.timeoutMs, maxStdoutBytes: this.config.maxStdoutBytes }).health(signal);
|
|
231
231
|
}
|
|
232
|
+
quickHealth(signal) {
|
|
233
|
+
const settings = this.store.settings;
|
|
234
|
+
return new OpenCliRunner({ executable: settings.opencliPath, profile: settings.profile,
|
|
235
|
+
timeoutMs: this.config.timeoutMs, maxStdoutBytes: this.config.maxStdoutBytes }).quickHealth(signal);
|
|
236
|
+
}
|
|
232
237
|
profiles(signal) {
|
|
233
238
|
const settings = this.store.settings;
|
|
234
239
|
return new OpenCliRunner({ executable: settings.opencliPath, profile: '',
|
package/lib/store/spec.d.ts
CHANGED
|
@@ -192,31 +192,44 @@ export declare const referenceAnythingDomainSpec: {
|
|
|
192
192
|
pill: "pill";
|
|
193
193
|
"raw-text": "raw-text";
|
|
194
194
|
}>>;
|
|
195
|
+
referenceUiMode: z.ZodOptional<z.ZodEnum<{
|
|
196
|
+
plugin: "plugin";
|
|
197
|
+
official: "official";
|
|
198
|
+
}>>;
|
|
195
199
|
picker: z.ZodOptional<z.ZodObject<{
|
|
200
|
+
displayMode: z.ZodDefault<z.ZodEnum<{
|
|
201
|
+
collapse: "collapse";
|
|
202
|
+
"native-scroll": "native-scroll";
|
|
203
|
+
}>>;
|
|
196
204
|
commands: z.ZodObject<{
|
|
197
205
|
enabled: z.ZodBoolean;
|
|
198
206
|
order: z.ZodNumber;
|
|
199
207
|
limit: z.ZodNumber;
|
|
208
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
200
209
|
}, z.core.$strip>;
|
|
201
210
|
skills: z.ZodObject<{
|
|
202
211
|
enabled: z.ZodBoolean;
|
|
203
212
|
order: z.ZodNumber;
|
|
204
213
|
limit: z.ZodNumber;
|
|
214
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
205
215
|
}, z.core.$strip>;
|
|
206
216
|
files: z.ZodObject<{
|
|
207
217
|
enabled: z.ZodBoolean;
|
|
208
218
|
order: z.ZodNumber;
|
|
209
219
|
limit: z.ZodNumber;
|
|
220
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
210
221
|
}, z.core.$strip>;
|
|
211
222
|
sessions: z.ZodObject<{
|
|
212
223
|
enabled: z.ZodBoolean;
|
|
213
224
|
order: z.ZodNumber;
|
|
214
225
|
limit: z.ZodNumber;
|
|
226
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
215
227
|
}, z.core.$strip>;
|
|
216
228
|
conversations: z.ZodObject<{
|
|
217
229
|
enabled: z.ZodBoolean;
|
|
218
230
|
order: z.ZodNumber;
|
|
219
231
|
limit: z.ZodNumber;
|
|
232
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
220
233
|
}, z.core.$strip>;
|
|
221
234
|
}, z.core.$strip>>;
|
|
222
235
|
}, z.core.$strip>;
|
package/lib/sync/index.js
CHANGED
|
@@ -109,9 +109,9 @@ export class ConversationSyncManager {
|
|
|
109
109
|
const failures = new Map();
|
|
110
110
|
let succeeded = 0;
|
|
111
111
|
try {
|
|
112
|
-
|
|
112
|
+
for (const provider of job.providers) {
|
|
113
113
|
if (job.controller.signal.aborted)
|
|
114
|
-
|
|
114
|
+
break;
|
|
115
115
|
const started = Date.now();
|
|
116
116
|
try {
|
|
117
117
|
await this.syncProvider(job, provider, mode, options);
|
|
@@ -123,11 +123,11 @@ export class ConversationSyncManager {
|
|
|
123
123
|
// logged-out account would otherwise permanently starve every
|
|
124
124
|
// provider after it in this list.
|
|
125
125
|
if (job.controller.signal.aborted)
|
|
126
|
-
|
|
126
|
+
break;
|
|
127
127
|
failures.set(provider, describe(error));
|
|
128
128
|
this.logger?.warn(`reference sync: ${provider} failed: ${describe(error)}`);
|
|
129
129
|
}
|
|
130
|
-
}
|
|
130
|
+
}
|
|
131
131
|
job.status = job.controller.signal.aborted ? 'cancelled'
|
|
132
132
|
: failures.size === 0 ? 'complete'
|
|
133
133
|
: succeeded === 0 ? 'failed' : 'partial';
|
package/lib/typert.js
CHANGED
|
@@ -5,10 +5,9 @@ export const TYPERT_MANIFEST = {
|
|
|
5
5
|
key: 'referenceAnything', exportName: 'ReferenceAnythingRemote',
|
|
6
6
|
description: 'Local conversation mirror, synchronization, and settings for @Conversation.', tags: [],
|
|
7
7
|
members: [
|
|
8
|
-
{ kind: 'method', name: 'workspaceSearch', signature: 'workspaceSearch(agent: Agent, signal: AbortSignal): Promise<WorkspaceEntry[]>' },
|
|
9
|
-
{ kind: 'method', name: 'sessionSearch', signature: 'sessionSearch(agent: Agent, input: SessionSearchInput, signal: AbortSignal): Promise<SessionCandidate[]>' },
|
|
10
8
|
{ kind: 'method', name: 'search', signature: 'search(input: SearchInput): Promise<SearchResult[]>' },
|
|
11
9
|
{ kind: 'method', name: 'health', signature: 'health(signal: AbortSignal): Promise<Health>' },
|
|
10
|
+
{ kind: 'method', name: 'quickHealth', signature: 'quickHealth(signal: AbortSignal): Promise<Health>' },
|
|
12
11
|
{ kind: 'method', name: 'profiles', signature: 'profiles(signal: AbortSignal): Promise<BrowserProfile[]>' },
|
|
13
12
|
{ kind: 'method', name: 'discoverOpenCli', signature: 'discoverOpenCli(signal: AbortSignal): Promise<OpenCliDiscovery>' },
|
|
14
13
|
{ kind: 'method', name: 'installOpenCli', signature: 'installOpenCli(signal: AbortSignal): Promise<OpenCliDiscovery>' },
|
|
@@ -17,6 +16,7 @@ export const TYPERT_MANIFEST = {
|
|
|
17
16
|
{ kind: 'method', name: 'updateStatus', signature: 'updateStatus(signal: AbortSignal): Promise<PackageUpdateStatus>' },
|
|
18
17
|
{ kind: 'method', name: 'checkUpdate', signature: 'checkUpdate(signal: AbortSignal): Promise<PackageUpdateStatus>' },
|
|
19
18
|
{ kind: 'method', name: 'installUpdate', signature: 'installUpdate(signal: AbortSignal): Promise<PackageUpdateResult>' },
|
|
19
|
+
{ kind: 'method', name: 'switchReferenceUiMode', signature: 'switchReferenceUiMode(input: ReferenceUiModeInput, signal: AbortSignal): Promise<ReferenceUiSwitchResult>' },
|
|
20
20
|
{ kind: 'method', name: 'stats', signature: 'stats(): ProviderStats[]' },
|
|
21
21
|
{ kind: 'method', name: 'syncStart', signature: 'syncStart(input: SyncStart): string' },
|
|
22
22
|
{ kind: 'method', name: 'syncStatus', signature: 'syncStatus(input: JobInput): SyncStatus | undefined' },
|
package/lib/web.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReferenceAnythingRemote } from "./host.js";
|
|
2
2
|
import { TYPERT_MANIFEST } from "./typert.js";
|
|
3
3
|
export const name = 'reference-anything-web';
|
|
4
|
-
export const inject = ['typert', 'referenceChatHistory'
|
|
4
|
+
export const inject = ['typert', 'referenceChatHistory'];
|
|
5
5
|
export function apply(ctx) {
|
|
6
6
|
new ReferenceAnythingRemote(ctx);
|
|
7
7
|
ctx.effect(() => {
|
package/lib/wire.d.ts
CHANGED
|
@@ -22,41 +22,57 @@ declare const pickerSourceSettingsSchema: z.ZodObject<{
|
|
|
22
22
|
enabled: z.ZodBoolean;
|
|
23
23
|
order: z.ZodNumber;
|
|
24
24
|
limit: z.ZodNumber;
|
|
25
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
25
26
|
}, z.core.$strip>;
|
|
26
27
|
export type PickerSourceSettings = z.infer<typeof pickerSourceSettingsSchema>;
|
|
27
28
|
export declare const pickerSettingsSchema: z.ZodObject<{
|
|
29
|
+
displayMode: z.ZodDefault<z.ZodEnum<{
|
|
30
|
+
collapse: "collapse";
|
|
31
|
+
"native-scroll": "native-scroll";
|
|
32
|
+
}>>;
|
|
28
33
|
commands: z.ZodObject<{
|
|
29
34
|
enabled: z.ZodBoolean;
|
|
30
35
|
order: z.ZodNumber;
|
|
31
36
|
limit: z.ZodNumber;
|
|
37
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
32
38
|
}, z.core.$strip>;
|
|
33
39
|
skills: z.ZodObject<{
|
|
34
40
|
enabled: z.ZodBoolean;
|
|
35
41
|
order: z.ZodNumber;
|
|
36
42
|
limit: z.ZodNumber;
|
|
43
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
37
44
|
}, z.core.$strip>;
|
|
38
45
|
files: z.ZodObject<{
|
|
39
46
|
enabled: z.ZodBoolean;
|
|
40
47
|
order: z.ZodNumber;
|
|
41
48
|
limit: z.ZodNumber;
|
|
49
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
42
50
|
}, z.core.$strip>;
|
|
43
51
|
sessions: z.ZodObject<{
|
|
44
52
|
enabled: z.ZodBoolean;
|
|
45
53
|
order: z.ZodNumber;
|
|
46
54
|
limit: z.ZodNumber;
|
|
55
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
47
56
|
}, z.core.$strip>;
|
|
48
57
|
conversations: z.ZodObject<{
|
|
49
58
|
enabled: z.ZodBoolean;
|
|
50
59
|
order: z.ZodNumber;
|
|
51
60
|
limit: z.ZodNumber;
|
|
61
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
52
62
|
}, z.core.$strip>;
|
|
53
63
|
}, z.core.$strip>;
|
|
54
64
|
export type PickerSettings = z.infer<typeof pickerSettingsSchema>;
|
|
65
|
+
export type PickerDisplayMode = PickerSettings['displayMode'];
|
|
55
66
|
export declare const inputRenderModeSchema: z.ZodEnum<{
|
|
56
67
|
pill: "pill";
|
|
57
68
|
"raw-text": "raw-text";
|
|
58
69
|
}>;
|
|
59
70
|
export type InputRenderMode = z.infer<typeof inputRenderModeSchema>;
|
|
71
|
+
export declare const referenceUiModeSchema: z.ZodEnum<{
|
|
72
|
+
plugin: "plugin";
|
|
73
|
+
official: "official";
|
|
74
|
+
}>;
|
|
75
|
+
export type ReferenceUiMode = z.infer<typeof referenceUiModeSchema>;
|
|
60
76
|
/** Defaults used before the user saves the General section. */
|
|
61
77
|
export declare function defaultPickerSettings(): PickerSettings;
|
|
62
78
|
/** Picker settings have a fixed schema, so explicit comparison is cheap and stable. */
|
|
@@ -85,31 +101,44 @@ export declare const settingsRecordSchema: z.ZodObject<{
|
|
|
85
101
|
pill: "pill";
|
|
86
102
|
"raw-text": "raw-text";
|
|
87
103
|
}>>;
|
|
104
|
+
referenceUiMode: z.ZodOptional<z.ZodEnum<{
|
|
105
|
+
plugin: "plugin";
|
|
106
|
+
official: "official";
|
|
107
|
+
}>>;
|
|
88
108
|
picker: z.ZodOptional<z.ZodObject<{
|
|
109
|
+
displayMode: z.ZodDefault<z.ZodEnum<{
|
|
110
|
+
collapse: "collapse";
|
|
111
|
+
"native-scroll": "native-scroll";
|
|
112
|
+
}>>;
|
|
89
113
|
commands: z.ZodObject<{
|
|
90
114
|
enabled: z.ZodBoolean;
|
|
91
115
|
order: z.ZodNumber;
|
|
92
116
|
limit: z.ZodNumber;
|
|
117
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
93
118
|
}, z.core.$strip>;
|
|
94
119
|
skills: z.ZodObject<{
|
|
95
120
|
enabled: z.ZodBoolean;
|
|
96
121
|
order: z.ZodNumber;
|
|
97
122
|
limit: z.ZodNumber;
|
|
123
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
98
124
|
}, z.core.$strip>;
|
|
99
125
|
files: z.ZodObject<{
|
|
100
126
|
enabled: z.ZodBoolean;
|
|
101
127
|
order: z.ZodNumber;
|
|
102
128
|
limit: z.ZodNumber;
|
|
129
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
103
130
|
}, z.core.$strip>;
|
|
104
131
|
sessions: z.ZodObject<{
|
|
105
132
|
enabled: z.ZodBoolean;
|
|
106
133
|
order: z.ZodNumber;
|
|
107
134
|
limit: z.ZodNumber;
|
|
135
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
108
136
|
}, z.core.$strip>;
|
|
109
137
|
conversations: z.ZodObject<{
|
|
110
138
|
enabled: z.ZodBoolean;
|
|
111
139
|
order: z.ZodNumber;
|
|
112
140
|
limit: z.ZodNumber;
|
|
141
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
113
142
|
}, z.core.$strip>;
|
|
114
143
|
}, z.core.$strip>>;
|
|
115
144
|
}, z.core.$strip>;
|
package/lib/wire.js
CHANGED
|
@@ -7,8 +7,10 @@ const pickerSourceSettingsSchema = z.object({
|
|
|
7
7
|
enabled: z.boolean(),
|
|
8
8
|
order: z.number().int().min(0).max(100),
|
|
9
9
|
limit: z.number().int().min(1).max(50),
|
|
10
|
+
maxCandidates: z.number().int().min(1).max(50).default(50),
|
|
10
11
|
});
|
|
11
12
|
export const pickerSettingsSchema = z.object({
|
|
13
|
+
displayMode: z.enum(['collapse', 'native-scroll']).default('collapse'),
|
|
12
14
|
commands: pickerSourceSettingsSchema,
|
|
13
15
|
skills: pickerSourceSettingsSchema,
|
|
14
16
|
files: pickerSourceSettingsSchema,
|
|
@@ -16,21 +18,26 @@ export const pickerSettingsSchema = z.object({
|
|
|
16
18
|
conversations: pickerSourceSettingsSchema,
|
|
17
19
|
});
|
|
18
20
|
export const inputRenderModeSchema = z.enum(['pill', 'raw-text']);
|
|
21
|
+
export const referenceUiModeSchema = z.enum(['plugin', 'official']);
|
|
19
22
|
/** Defaults used before the user saves the General section. */
|
|
20
23
|
export function defaultPickerSettings() {
|
|
21
24
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
displayMode: 'collapse',
|
|
26
|
+
commands: { enabled: true, order: 0, limit: 6, maxCandidates: 50 },
|
|
27
|
+
skills: { enabled: true, order: 5, limit: 6, maxCandidates: 50 },
|
|
28
|
+
files: { enabled: true, order: 10, limit: 6, maxCandidates: 50 },
|
|
29
|
+
sessions: { enabled: true, order: 20, limit: 6, maxCandidates: 50 },
|
|
30
|
+
conversations: { enabled: true, order: 30, limit: 6, maxCandidates: 50 },
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
33
|
/** Picker settings have a fixed schema, so explicit comparison is cheap and stable. */
|
|
30
34
|
export function samePickerSettings(left, right) {
|
|
31
|
-
|
|
35
|
+
if (left.displayMode !== right.displayMode)
|
|
36
|
+
return false;
|
|
37
|
+
return pickerSourceSchema.options.every(key => left[key].enabled === right[key].enabled &&
|
|
32
38
|
left[key].order === right[key].order &&
|
|
33
|
-
left[key].limit === right[key].limit
|
|
39
|
+
left[key].limit === right[key].limit &&
|
|
40
|
+
left[key].maxCandidates === right[key].maxCandidates);
|
|
34
41
|
}
|
|
35
42
|
export const settingsRecordSchema = z.object({
|
|
36
43
|
opencliPath: z.string(),
|
|
@@ -43,6 +50,8 @@ export const settingsRecordSchema = z.object({
|
|
|
43
50
|
enabledProviders: z.array(providerSchema).default([...ALL_PROVIDERS]),
|
|
44
51
|
maxReadTurns: z.number().int().min(1).max(100).default(10),
|
|
45
52
|
inputRenderMode: inputRenderModeSchema.default('pill'),
|
|
53
|
+
// Optional keeps settings written by earlier plugin versions readable.
|
|
54
|
+
referenceUiMode: referenceUiModeSchema.optional(),
|
|
46
55
|
// Optional keeps existing on-disk settings forward-compatible. The client
|
|
47
56
|
// uses defaultPickerSettings() until the user saves the General section.
|
|
48
57
|
picker: pickerSettingsSchema.optional(),
|
package/opencli-plugin/common.js
CHANGED
|
@@ -38,7 +38,7 @@ async function evaluate(page, script, args, operation) {
|
|
|
38
38
|
return parseEvaluate(await page.evaluate(source), operation)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function assertResult(result, domain, operation) {
|
|
41
|
+
function assertResult(result, domain, operation) {
|
|
42
42
|
if (result?.code === 'AUTH') throw new AuthRequiredError(domain, result.message || `${operation} requires login`)
|
|
43
43
|
if (result?.code === 'RATE_LIMIT') {
|
|
44
44
|
throw new CommandExecutionError(`${operation}: ${result.message || 'provider rate limit reached'}`)
|
|
@@ -46,9 +46,70 @@ function assertResult(result, domain, operation) {
|
|
|
46
46
|
if (result?.ok !== true || !Array.isArray(result.rows)) {
|
|
47
47
|
throw new CommandExecutionError(`${operation}: ${result?.message || 'provider adapter did not return rows'}`)
|
|
48
48
|
}
|
|
49
|
-
return result.rows
|
|
50
|
-
}
|
|
51
|
-
|
|
49
|
+
return result.rows
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PROVIDER_READY_TIMEOUT_MS = 1200
|
|
53
|
+
const PROVIDER_READY_INITIAL_DELAY_MS = 50
|
|
54
|
+
const PROVIDER_READY_MAX_DELAY_MS = 400
|
|
55
|
+
|
|
56
|
+
function terminalProviderResult(result) {
|
|
57
|
+
return result?.ok === true || result?.code === 'AUTH' || result?.code === 'RATE_LIMIT'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function terminalIdentityResult(result) {
|
|
61
|
+
return result?.code === 'AUTH' || result?.code === 'RATE_LIMIT'
|
|
62
|
+
|| (result?.ok === true && typeof result.identity === 'string' && result.identity.length > 0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run the provider API as the readiness probe. The happy path performs one
|
|
67
|
+
* evaluate immediately after navigation; transient bootstrap failures retry
|
|
68
|
+
* with bounded exponential backoff instead of waiting for the whole SPA DOM
|
|
69
|
+
* to become quiet first.
|
|
70
|
+
*/
|
|
71
|
+
export async function evaluateWhenProviderReady(page, script, args, operation, options = {}) {
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? PROVIDER_READY_TIMEOUT_MS
|
|
73
|
+
const maxDelayMs = options.maxDelayMs ?? PROVIDER_READY_MAX_DELAY_MS
|
|
74
|
+
const accepts = options.accepts ?? terminalProviderResult
|
|
75
|
+
const deadline = Date.now() + timeoutMs
|
|
76
|
+
let delayMs = options.initialDelayMs ?? PROVIDER_READY_INITIAL_DELAY_MS
|
|
77
|
+
let lastResult
|
|
78
|
+
let lastError
|
|
79
|
+
for (;;) {
|
|
80
|
+
try {
|
|
81
|
+
lastResult = await evaluate(page, script, args, operation)
|
|
82
|
+
lastError = undefined
|
|
83
|
+
if (accepts(lastResult)) return lastResult
|
|
84
|
+
} catch (error) {
|
|
85
|
+
lastError = error
|
|
86
|
+
}
|
|
87
|
+
const remaining = deadline - Date.now()
|
|
88
|
+
if (remaining <= 0) {
|
|
89
|
+
if (lastError) throw lastError
|
|
90
|
+
return lastResult
|
|
91
|
+
}
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(delayMs, remaining)))
|
|
93
|
+
delayMs = Math.min(Math.max(delayMs * 2, 1), maxDelayMs)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Each command owns an ephemeral Browser Bridge tab. Close that tab after the
|
|
99
|
+
* command finishes while preserving the provider result or failure.
|
|
100
|
+
*/
|
|
101
|
+
async function inTemporaryTab(page, task) {
|
|
102
|
+
try {
|
|
103
|
+
return await task()
|
|
104
|
+
} finally {
|
|
105
|
+
try {
|
|
106
|
+
await page.closeTab?.()
|
|
107
|
+
} catch {
|
|
108
|
+
// Preserve the provider result/error if the tab or browser closed first.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
52
113
|
export function registerProvider(config) {
|
|
53
114
|
const browserSession = SYNC_BROWSER_SESSION
|
|
54
115
|
|
|
@@ -63,16 +124,18 @@ export function registerProvider(config) {
|
|
|
63
124
|
...browserSession,
|
|
64
125
|
navigateBefore: false,
|
|
65
126
|
args: [],
|
|
66
|
-
columns: ['identity'],
|
|
67
|
-
func: async (page) => {
|
|
68
|
-
await page.goto(config.home, {
|
|
69
|
-
const result = await
|
|
127
|
+
columns: ['identity'],
|
|
128
|
+
func: async (page) => inTemporaryTab(page, async () => {
|
|
129
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
130
|
+
const result = await evaluateWhenProviderReady(
|
|
131
|
+
page, config.whoamiScript, {}, `${config.site} whoami`, { accepts: terminalIdentityResult },
|
|
132
|
+
)
|
|
70
133
|
if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
71
134
|
if (result?.ok !== true || typeof result.identity !== 'string' || !result.identity) {
|
|
72
135
|
throw new CommandExecutionError(`${config.site} whoami: ${result?.message || 'stable account identity unavailable'}`)
|
|
73
136
|
}
|
|
74
137
|
return [{ identity: result.identity }]
|
|
75
|
-
},
|
|
138
|
+
}),
|
|
76
139
|
})
|
|
77
140
|
|
|
78
141
|
cli({
|
|
@@ -90,11 +153,13 @@ export function registerProvider(config) {
|
|
|
90
153
|
help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
|
|
91
154
|
}, { name: 'accountScope', help: 'Previously observed hashed account scope; mismatch forces a full listing' }],
|
|
92
155
|
columns: SYNC_INDEX_COLUMNS,
|
|
93
|
-
func: async (page, kwargs) => {
|
|
156
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
94
157
|
const since = String(kwargs.since || '').trim()
|
|
95
158
|
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
96
|
-
await page.goto(config.home, {
|
|
97
|
-
const identity = await
|
|
159
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
160
|
+
const identity = await evaluateWhenProviderReady(
|
|
161
|
+
page, config.whoamiScript, {}, `${config.site} sync-index identity`, { accepts: terminalIdentityResult },
|
|
162
|
+
)
|
|
98
163
|
if (identity?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
99
164
|
if (identity?.ok !== true || typeof identity.identity !== 'string' || !identity.identity) {
|
|
100
165
|
throw new CommandExecutionError(`${config.site} sync-index: ${identity?.message || 'stable account identity unavailable'}`)
|
|
@@ -110,7 +175,7 @@ export function registerProvider(config) {
|
|
|
110
175
|
{ kind: 'identity', identity: identity.identity, sinceApplied: effectiveSince },
|
|
111
176
|
...rows.map(row => ({ kind: 'conversation', identity: '', sinceApplied: '', ...row })),
|
|
112
177
|
]
|
|
113
|
-
},
|
|
178
|
+
}),
|
|
114
179
|
})
|
|
115
180
|
|
|
116
181
|
cli({
|
|
@@ -128,17 +193,17 @@ export function registerProvider(config) {
|
|
|
128
193
|
help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
|
|
129
194
|
}],
|
|
130
195
|
columns: HISTORY_COLUMNS,
|
|
131
|
-
func: async (page, kwargs) => {
|
|
196
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
132
197
|
const since = String(kwargs.since || '').trim()
|
|
133
|
-
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
134
|
-
await page.goto(config.home, {
|
|
135
|
-
const rows = assertResult(
|
|
136
|
-
await
|
|
137
|
-
config.domain,
|
|
138
|
-
`${config.site} history-all`,
|
|
198
|
+
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
199
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
200
|
+
const rows = assertResult(
|
|
201
|
+
await evaluateWhenProviderReady(page, config.historyScript, { since }, `${config.site} history-all`),
|
|
202
|
+
config.domain,
|
|
203
|
+
`${config.site} history-all`,
|
|
139
204
|
)
|
|
140
205
|
return rows
|
|
141
|
-
},
|
|
206
|
+
}),
|
|
142
207
|
})
|
|
143
208
|
|
|
144
209
|
cli({
|
|
@@ -156,7 +221,7 @@ export function registerProvider(config) {
|
|
|
156
221
|
{ name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the read' },
|
|
157
222
|
],
|
|
158
223
|
columns: DETAIL_COLUMNS,
|
|
159
|
-
func: async (page, kwargs) => {
|
|
224
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
160
225
|
const id = String(kwargs.id || '').trim()
|
|
161
226
|
if (!id) throw new ArgumentError('id must be a non-empty conversation id')
|
|
162
227
|
await page.goto(config.home, { settleMs: 600 })
|
|
@@ -180,7 +245,7 @@ export function registerProvider(config) {
|
|
|
180
245
|
const rows = assertResult(result, config.domain, `${config.site} detail`)
|
|
181
246
|
if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
|
|
182
247
|
return rows
|
|
183
|
-
},
|
|
248
|
+
}),
|
|
184
249
|
})
|
|
185
250
|
|
|
186
251
|
cli({
|
|
@@ -199,7 +264,7 @@ export function registerProvider(config) {
|
|
|
199
264
|
{ name: 'maxBytes', type: 'int', default: 26214400, help: 'Maximum decoded bytes' },
|
|
200
265
|
],
|
|
201
266
|
columns: ['attachmentId', 'name', 'mimeType', 'size', 'status', 'localPath'],
|
|
202
|
-
func: async (page, kwargs) => {
|
|
267
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
203
268
|
const locator = String(kwargs.locator || '').trim()
|
|
204
269
|
const output = resolve(String(kwargs.output || ''))
|
|
205
270
|
const maxBytes = Number(kwargs.maxBytes ?? 26_214_400)
|
|
@@ -229,18 +294,13 @@ export function registerProvider(config) {
|
|
|
229
294
|
status: 'available',
|
|
230
295
|
localPath: output,
|
|
231
296
|
}]
|
|
232
|
-
},
|
|
233
|
-
})
|
|
297
|
+
}),
|
|
298
|
+
})
|
|
234
299
|
}
|
|
235
300
|
|
|
236
|
-
/**
|
|
237
|
-
* Keep one stable, background Browser Bridge session per provider site.
|
|
238
|
-
* OpenCLI maps persistent sessions to `site:<config.site>` and keeps their tab
|
|
239
|
-
* lease after both successful and failed commands, so later sync work reuses
|
|
240
|
-
* the same tab. If the user closes it manually, OpenCLI recreates it on demand.
|
|
241
|
-
*/
|
|
301
|
+
/** Browser Bridge lifecycle shared by every web-chat command. */
|
|
242
302
|
export const SYNC_BROWSER_SESSION = Object.freeze({
|
|
243
|
-
siteSession: '
|
|
303
|
+
siteSession: 'ephemeral',
|
|
244
304
|
defaultWindowMode: 'background',
|
|
245
305
|
})
|
|
246
306
|
|