dsh-reference-anything 0.2.5 → 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/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
- * Two schemes are expanded. `dsh-ref:` names material outside the harness and
12
- * is resolved through `ctx.references`. `dsh-session:` names another harness
13
- * session and is delegated to `ctx.sessionReferenceResolver`, which owns
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(text => mentions(text, serveSessionScheme)));
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, serveSessionScheme);
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 && parsed.sessionReferences.length === 0 && parsed.workspaceReferences.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, { maxReferences, serveSessionScheme }, signal);
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, limits, signal) {
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 > limits.maxReferences) {
108
- throw new ReferenceAnythingError(`a message may reference at most ${limits.maxReferences} outside conversation`
109
- + `${limits.maxReferences === 1 ? '' : 's'}; this one named ${inputs.length}`, 'REFERENCE_TOO_MANY');
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 both schemes' mentions to readable text, preserving message identity. */
138
- function parseMessage(message, serveSessionScheme) {
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
- let text = own.text;
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: '',
@@ -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
- await Promise.all(job.providers.map(async (provider) => {
112
+ for (const provider of job.providers) {
113
113
  if (job.controller.signal.aborted)
114
- return;
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
- return;
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', 'sessionReferenceResolver'];
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
- commands: { enabled: true, order: 0, limit: 6 },
23
- skills: { enabled: true, order: 5, limit: 6 },
24
- files: { enabled: true, order: 10, limit: 6 },
25
- sessions: { enabled: true, order: 20, limit: 6 },
26
- conversations: { enabled: true, order: 30, limit: 6 },
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
- return Object.keys(left).every(key => left[key].enabled === right[key].enabled &&
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(),
@@ -49,10 +49,54 @@ function assertResult(result, domain, operation) {
49
49
  return result.rows
50
50
  }
51
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
+
52
97
  /**
53
- * Close only the tab used by this adapter command. The persistent site session
54
- * prevents OpenCLI from sending its `close-window` lease-release action, while
55
- * Browser Bridge can keep and reuse the background container window.
98
+ * Each command owns an ephemeral Browser Bridge tab. Close that tab after the
99
+ * command finishes while preserving the provider result or failure.
56
100
  */
57
101
  async function inTemporaryTab(page, task) {
58
102
  try {
@@ -80,10 +124,12 @@ export function registerProvider(config) {
80
124
  ...browserSession,
81
125
  navigateBefore: false,
82
126
  args: [],
83
- columns: ['identity'],
127
+ columns: ['identity'],
84
128
  func: async (page) => inTemporaryTab(page, async () => {
85
- await page.goto(config.home, { settleMs: 500 })
86
- const result = await evaluate(page, config.whoamiScript, {}, `${config.site} whoami`)
129
+ await page.goto(config.home, { waitUntil: 'none' })
130
+ const result = await evaluateWhenProviderReady(
131
+ page, config.whoamiScript, {}, `${config.site} whoami`, { accepts: terminalIdentityResult },
132
+ )
87
133
  if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
88
134
  if (result?.ok !== true || typeof result.identity !== 'string' || !result.identity) {
89
135
  throw new CommandExecutionError(`${config.site} whoami: ${result?.message || 'stable account identity unavailable'}`)
@@ -110,8 +156,10 @@ export function registerProvider(config) {
110
156
  func: async (page, kwargs) => inTemporaryTab(page, async () => {
111
157
  const since = String(kwargs.since || '').trim()
112
158
  if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
113
- await page.goto(config.home, { settleMs: 1200 })
114
- const identity = await evaluate(page, config.whoamiScript, {}, `${config.site} sync-index identity`)
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
+ )
115
163
  if (identity?.code === 'AUTH') throw new AuthRequiredError(config.domain)
116
164
  if (identity?.ok !== true || typeof identity.identity !== 'string' || !identity.identity) {
117
165
  throw new CommandExecutionError(`${config.site} sync-index: ${identity?.message || 'stable account identity unavailable'}`)
@@ -147,12 +195,12 @@ export function registerProvider(config) {
147
195
  columns: HISTORY_COLUMNS,
148
196
  func: async (page, kwargs) => inTemporaryTab(page, async () => {
149
197
  const since = String(kwargs.since || '').trim()
150
- if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
151
- await page.goto(config.home, { settleMs: 1200 })
152
- const rows = assertResult(
153
- await evaluate(page, config.historyScript, { since }, `${config.site} history-all`),
154
- config.domain,
155
- `${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`,
156
204
  )
157
205
  return rows
158
206
  }),
@@ -247,16 +295,12 @@ export function registerProvider(config) {
247
295
  localPath: output,
248
296
  }]
249
297
  }),
250
- })
298
+ })
251
299
  }
252
300
 
253
- /**
254
- * Keep one stable, background Browser Bridge session per provider site.
255
- * Persistence suppresses OpenCLI's `close-window` action; inTemporaryTab closes
256
- * only the finished command's tab so the reusable container window stays alive.
257
- */
301
+ /** Browser Bridge lifecycle shared by every web-chat command. */
258
302
  export const SYNC_BROWSER_SESSION = Object.freeze({
259
- siteSession: 'persistent',
303
+ siteSession: 'ephemeral',
260
304
  defaultWindowMode: 'background',
261
305
  })
262
306
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-chat-history",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "opencli": ">=1.8.6",
5
5
  "description": "Full-history read adapters used by dsh-reference-anything"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencli-plugin-dsh-chat-history",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@jackwener/opencli": ">=1.8.6"