@dsh-electron/dsh-plugin-multi-root-workspace 0.1.10 → 0.1.12

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.
Files changed (60) hide show
  1. package/CHANGELOG.en.md +25 -1
  2. package/CHANGELOG.md +25 -1
  3. package/README.en.md +11 -9
  4. package/README.md +11 -9
  5. package/cordis.patch.yml +3 -2
  6. package/lib/auto-review.js +1 -1
  7. package/lib/client.js +252 -129
  8. package/lib/client.js.map +1 -1
  9. package/lib/{command-BGdz7VBH.js → command-CmLekG1M.js} +3 -2
  10. package/lib/command.js +1 -1
  11. package/lib/{compat-D-MljsPV.js → compat-Bg2HFVMX.js} +6 -1
  12. package/lib/compat.js +1 -1
  13. package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
  14. package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
  15. package/lib/fs.js +1 -1
  16. package/lib/index.js +7 -6
  17. package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
  18. package/lib/instructions.js +1 -1
  19. package/lib/lsp.js +19 -16
  20. package/lib/native-win32-2waCRkPI.js +312 -0
  21. package/lib/process-fence.js +62 -12
  22. package/lib/protocol-4zdwV7u4.js +641 -0
  23. package/lib/{registry-KSsmU393.js → registry-YTUOjmVu.js} +368 -25
  24. package/lib/registry-operation-C2ZV3Jhw.js +116 -0
  25. package/lib/registry.js +3 -2
  26. package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
  27. package/lib/review-context.js +1 -1
  28. package/lib/sandbox-C-vCJCp3.js +278 -0
  29. package/lib/sandbox.js +1 -1
  30. package/lib/scope.js +70 -10
  31. package/lib/skills.js +48 -29
  32. package/lib/types/authority/native-win32.d.ts +7 -0
  33. package/lib/types/authority/native.d.ts +23 -0
  34. package/lib/types/authority/protocol.d.ts +70 -0
  35. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +5 -3
  36. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +16 -10
  37. package/lib/types/client/open-in-app/adapter.d.ts +5 -1
  38. package/lib/types/client/open-in-app/controller.d.ts +5 -2
  39. package/lib/types/client/open-in-app/device.d.ts +11 -0
  40. package/lib/types/client/open-in-app/locales.d.ts +1 -1
  41. package/lib/types/client/open-in-app/open-path.d.ts +6 -5
  42. package/lib/types/client/open-in-app/open-toast.d.ts +39 -0
  43. package/lib/types/client/open-in-app/outcome.d.ts +23 -0
  44. package/lib/types/client/styles.d.ts +1 -1
  45. package/lib/types/compat/authority-storage.d.ts +6 -0
  46. package/lib/types/compat/dsh-version.d.ts +4 -4
  47. package/lib/types/compat/open-in-app-device.d.ts +23 -0
  48. package/lib/types/compat/process-entrypoints.d.ts +2 -0
  49. package/lib/types/compat/process-profile.d.ts +3 -0
  50. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  51. package/lib/types/fs.d.ts +4 -1
  52. package/lib/types/instructions.d.ts +0 -47
  53. package/lib/types/registry-operation.d.ts +31 -0
  54. package/lib/types/registry.d.ts +47 -6
  55. package/lib/types/scope.d.ts +12 -0
  56. package/lib/types/skills/catalog.d.ts +3 -0
  57. package/lib/types/skills/pool.d.ts +1 -1
  58. package/lib/workspace-files.js +63 -7
  59. package/package.json +87 -80
  60. package/lib/types/client/open-in-app/open-failure-toast.d.ts +0 -11
package/lib/skills.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
2
+ import { c as withRegistryRead } from "./registry-operation-C2ZV3Jhw.js";
2
3
  import { r as isPackageInstalled, t as createInstructionMessage } from "./llm-message-fYORacvh.js";
3
4
  import { n as skillsMetadataValid } from "./metadata-DgqSdVX_.js";
4
5
  import { constants, statSync } from "node:fs";
@@ -6,8 +7,8 @@ import { createHash } from "node:crypto";
6
7
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
7
8
  import { Service } from "@deepseek-ai/cordis";
8
9
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
9
- import { lstat, open, readdir, stat } from "node:fs/promises";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { lstat, open, readdir, stat } from "node:fs/promises";
11
12
  import z from "@deepseek-ai/schemastery";
12
13
  //#region \0@oxc-project+runtime@0.149.0/helpers/esm/usingCtx.js
13
14
  function _usingCtx() {
@@ -191,9 +192,10 @@ var SkillReaderPool = class {
191
192
  return this.entries.size;
192
193
  }
193
194
  get roots() {
194
- return [...this.entries.keys()];
195
+ return [...new Set([...this.entries.values()].map((entry) => entry.root))];
195
196
  }
196
- async use(key, create, run, signal) {
197
+ async use(root, create, run, signal, identity = root) {
198
+ const key = identity;
197
199
  for (;;) {
198
200
  signal?.throwIfAborted();
199
201
  if (this.closing) throw new Error("multi-root skills: reader pool is closing");
@@ -225,6 +227,7 @@ var SkillReaderPool = class {
225
227
  const controller = new AbortController();
226
228
  entry = {
227
229
  key,
230
+ root,
228
231
  controller,
229
232
  reader: create(controller.signal),
230
233
  busy: 0
@@ -235,7 +238,7 @@ var SkillReaderPool = class {
235
238
  this.entries.set(key, entry);
236
239
  entry.busy++;
237
240
  try {
238
- if (entry.stamp === void 0) entry.stamp = await skillTreeStamp(key);
241
+ if (entry.stamp === void 0) entry.stamp = await skillTreeStamp(entry.root);
239
242
  signal?.throwIfAborted();
240
243
  return await run(await entry.reader);
241
244
  } finally {
@@ -249,7 +252,7 @@ var SkillReaderPool = class {
249
252
  this.polling ??= (async () => {
250
253
  let changed = false;
251
254
  for (const entry of [...this.entries.values()]) {
252
- const stamp = await skillTreeStamp(entry.key);
255
+ const stamp = await skillTreeStamp(entry.root);
253
256
  if (entry.stamp !== void 0 && entry.stamp !== stamp) changed = true;
254
257
  entry.stamp = stamp;
255
258
  }
@@ -285,10 +288,10 @@ var SkillReaderPool = class {
285
288
  /** No symlink target outside the Root is read or enumerated. Content reads stay on demand. */
286
289
  async function skillTreeStamp(root) {
287
290
  const rows = [canonicalPath(root)];
288
- const stamp = async (path) => {
291
+ const stamp = async (path, identityOnly = false) => {
289
292
  try {
290
293
  const link = await lstat(path, { bigint: true });
291
- rows.push(`${path}:${link.dev}:${link.ino}:${link.size}:${link.mtimeNs}:${link.ctimeNs}:${canonicalPath(path)}`);
294
+ rows.push(identityOnly ? `${path}:${link.dev}:${link.ino}:${link.mode}:${canonicalPath(path)}` : `${path}:${link.dev}:${link.ino}:${link.size}:${link.mtimeNs}:${link.ctimeNs}:${canonicalPath(path)}`);
292
295
  if (!containsPath(root, canonicalPath(path))) return void 0;
293
296
  return await stat(path);
294
297
  } catch {
@@ -296,7 +299,7 @@ async function skillTreeStamp(root) {
296
299
  return;
297
300
  }
298
301
  };
299
- await stamp(root);
302
+ await stamp(root, true);
300
303
  for (const directory of [".dsh/skills", ".agents/skills"]) {
301
304
  const path = join(root, directory);
302
305
  if (!(await stamp(path))?.isDirectory()) continue;
@@ -336,7 +339,7 @@ var AdditionalSkillCatalog = class {
336
339
  return this.generation();
337
340
  }
338
341
  current(primary, id) {
339
- if (this.signal.aborted || !this.enabled() || this.ctx.multiRootRegistry.authority.kind !== "active" || this.ctx.multiRootScope.resolve({
342
+ if (this.signal.aborted || !this.enabled() || !this.ctx.multiRootRegistry.available || this.ctx.multiRootScope.snapshot({
340
343
  mode: "read-only",
341
344
  workspaceRoot: primary
342
345
  }).authority !== "available") return void 0;
@@ -355,13 +358,17 @@ var AdditionalSkillCatalog = class {
355
358
  if (root === void 0) return await run(void 0);
356
359
  return await this.requests.run({
357
360
  primary,
358
- id
361
+ id,
362
+ epoch: this.ctx.multiRootRegistry.epoch
359
363
  }, () => this.pool.use(root.path, (signal) => rootReader(this.ctx, root.path, AbortSignal.any([signal, this.signal]), () => {
360
364
  const request = this.requests.getStore();
361
- return request !== void 0 && this.current(request.primary, request.id)?.path === root.path;
362
- }), run, options.signal));
365
+ return request !== void 0 && request.epoch === this.ctx.multiRootRegistry.epoch && this.current(request.primary, request.id)?.path === root.path;
366
+ }), run, options.signal, `${this.ctx.multiRootRegistry.epoch ?? "unavailable"}\0${root.path}`));
363
367
  }
364
368
  async list(primary, options) {
369
+ return withRegistryRead(this.ctx, primary, "skills", () => this.listAdmitted(primary, options));
370
+ }
371
+ async listAdmitted(primary, options) {
365
372
  options.signal?.throwIfAborted();
366
373
  if (!this.enabled()) return [];
367
374
  const roots = await this.ctx.multiRootRegistry.refresh(primary);
@@ -381,6 +388,7 @@ var AdditionalSkillCatalog = class {
381
388
  seen.add(candidate.name);
382
389
  const locator = {
383
390
  primary,
391
+ authorityEpoch: this.ctx.multiRootRegistry.epoch,
384
392
  rootId: root.id,
385
393
  aliasId: root.aliasId,
386
394
  candidate
@@ -408,8 +416,11 @@ var AdditionalSkillCatalog = class {
408
416
  }
409
417
  /** Locators are created here and are not accepted from a client path or object. */
410
418
  async load(primary, entry, options, user) {
419
+ return withRegistryRead(this.ctx, primary, "skills", () => this.loadAdmitted(primary, entry, options, user));
420
+ }
421
+ async loadAdmitted(primary, entry, options, user) {
411
422
  const locator = entry.locator;
412
- if (typeof locator !== "object" || locator === null || !this.locators.has(locator) || locator.primary !== primary) return void 0;
423
+ if (typeof locator !== "object" || locator === null || !this.locators.has(locator) || locator.primary !== primary || locator.authorityEpoch !== this.ctx.multiRootRegistry.epoch) return void 0;
413
424
  const root = this.current(primary, locator.rootId);
414
425
  if (root === void 0 || root.aliasId !== locator.aliasId || entry.name !== internalSkillName(primary, root.id, locator.candidate.name)) return void 0;
415
426
  const revision = this.generation();
@@ -446,14 +457,16 @@ var AdditionalSkillCatalog = class {
446
457
  }
447
458
  async admissionValid(definition) {
448
459
  const receipt = this.loadedReceipts.get(definition);
449
- if (receipt === void 0) return false;
450
- const current = this.current(receipt.locator.primary, receipt.locator.rootId);
451
- if (current === void 0 || current.aliasId !== receipt.locator.aliasId || receipt.revision !== this.generation()) return false;
452
- try {
453
- return receipt.stamp === await fileStamp(definition.path) && receipt.revision === this.generation() && this.current(receipt.locator.primary, receipt.locator.rootId)?.aliasId === receipt.locator.aliasId;
454
- } catch {
455
- return false;
456
- }
460
+ if (receipt === void 0 || receipt.locator.authorityEpoch !== this.ctx.multiRootRegistry.epoch) return false;
461
+ return withRegistryRead(this.ctx, receipt.locator.primary, "skills", async () => {
462
+ const current = this.current(receipt.locator.primary, receipt.locator.rootId);
463
+ if (current === void 0 || current.aliasId !== receipt.locator.aliasId || receipt.revision !== this.generation()) return false;
464
+ try {
465
+ return receipt.stamp === await fileStamp(definition.path) && receipt.revision === this.generation() && this.current(receipt.locator.primary, receipt.locator.rootId)?.aliasId === receipt.locator.aliasId;
466
+ } catch {
467
+ return false;
468
+ }
469
+ });
457
470
  }
458
471
  async dispose() {
459
472
  await this.pool.dispose();
@@ -574,16 +587,22 @@ function installSkillRouting(ctx) {
574
587
  fibers.push(ctx.inject(["skills"], (scope) => {
575
588
  const registry = scope.skills;
576
589
  scope.effect(() => wrapMethod(registry, "snapshot", (original) => async function(options = {}) {
577
- const result = await original.call(this, optionsFor(options));
578
- const primary = skillOrigin(scope, options);
579
- if (primary !== void 0) for (const skill of result.skills) {
580
- if (skill.provider === "multi-root-workspace") continue;
581
- if (scope.multiRootRegistry.list(primary).some((root) => skill.name.startsWith(internalSkillName(primary, root.id, "")))) scope.logger.warn(`multi-root skills: internal ID conflict; native winner retained: ${skill.name} (${skill.provider})`);
582
- }
583
- return result;
590
+ const primaryOrigin = skillOrigin(scope, options) ?? cold.getStore();
591
+ const run = async () => {
592
+ const result = await original.call(this, optionsFor(options));
593
+ const primary = skillOrigin(scope, options);
594
+ if (primary !== void 0) for (const skill of result.skills) {
595
+ if (skill.provider === "multi-root-workspace") continue;
596
+ if (scope.multiRootRegistry.list(primary).some((root) => skill.name.startsWith(internalSkillName(primary, root.id, "")))) scope.logger.warn(`multi-root skills: internal ID conflict; native winner retained: ${skill.name} (${skill.provider})`);
597
+ }
598
+ return result;
599
+ };
600
+ return primaryOrigin === void 0 ? run() : withRegistryRead(scope, primaryOrigin, "skills", run);
584
601
  }));
585
602
  scope.effect(() => wrapMethod(registry, "get", (original) => function(name, options = {}) {
586
- return original.call(this, name, optionsFor(options));
603
+ const primary = skillOrigin(scope, options) ?? cold.getStore();
604
+ const run = () => original.call(this, name, optionsFor(options));
605
+ return primary === void 0 ? run() : withRegistryRead(scope, primary, "skills", run);
587
606
  }));
588
607
  }));
589
608
  fibers.push(ctx.inject(["sessionSkillCatalog", "sessionQuery"], (scope) => {
@@ -0,0 +1,7 @@
1
+ import type { Channel, Listener } from './native.ts';
2
+ export declare function connectWindows(endpoint: string): Promise<Channel>;
3
+ export declare function listenWindows(endpoint: string, accepted: (channel: Channel) => void): Promise<Listener>;
4
+ export declare function windowsHostAvailability(): Promise<{
5
+ available: boolean;
6
+ reason?: string;
7
+ }>;
@@ -0,0 +1,23 @@
1
+ import type { EventEmitter } from 'node:events';
2
+ export interface Channel {
3
+ on: EventEmitter['on'];
4
+ readonly writableLength?: number;
5
+ write(data: string): boolean;
6
+ destroy(): unknown;
7
+ }
8
+ export interface Listener {
9
+ close(): void;
10
+ }
11
+ export declare function storeIdentity(root: string): {
12
+ root: string;
13
+ id: string;
14
+ endpoint: string;
15
+ };
16
+ export declare function connectNative(endpoint: string): Promise<Channel>;
17
+ /** Called only by the kernel lease holder. Never removes another epoch's socket. */
18
+ export declare function listenNative(endpoint: string, accepted: (channel: Channel) => void): Promise<Listener>;
19
+ /** Capability probe for native tests and diagnostics; not an authorization bypass. */
20
+ export declare function nativeHostAvailability(): Promise<{
21
+ available: boolean;
22
+ reason?: string;
23
+ }>;
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ declare const methods: readonly ["hello", "snapshot", "admit", "add", "removeAt", "setAccess", "setAlias", "setSkillsEnabled", "setAliasId", "move", "recheck"];
3
+ export type Method = typeof methods[number];
4
+ export declare const snapshotSchema: z.ZodObject<{
5
+ protocol: z.ZodLiteral<1>;
6
+ store: z.ZodString;
7
+ epoch: z.ZodString;
8
+ revision: z.ZodNumber;
9
+ primary: z.ZodString;
10
+ roots: z.ZodArray<z.ZodObject<{
11
+ id: z.ZodString;
12
+ path: z.ZodString;
13
+ access: z.ZodEnum<{
14
+ inherit: "inherit";
15
+ "read-only": "read-only";
16
+ "read-write": "read-write";
17
+ }>;
18
+ addedAt: z.ZodString;
19
+ recordedPath: z.ZodOptional<z.ZodString>;
20
+ readOnlyRecordedPath: z.ZodOptional<z.ZodString>;
21
+ readWriteRecordedPath: z.ZodOptional<z.ZodString>;
22
+ alias: z.ZodOptional<z.ZodString>;
23
+ aliasId: z.ZodOptional<z.ZodUnknown>;
24
+ skillsEnabled: z.ZodOptional<z.ZodUnknown>;
25
+ state: z.ZodEnum<{
26
+ available: "available";
27
+ missing: "missing";
28
+ redirected: "redirected";
29
+ invalid: "invalid";
30
+ }>;
31
+ detail: z.ZodOptional<z.ZodString>;
32
+ invalidReason: z.ZodOptional<z.ZodLiteral<"membership-conflict">>;
33
+ }, z.core.$strict>>;
34
+ available: z.ZodBoolean;
35
+ }, z.core.$strict>;
36
+ export type Snapshot = z.infer<typeof snapshotSchema>;
37
+ export declare class AuthorityRpcError extends Error {
38
+ readonly code: string;
39
+ constructor(code: string, message: string);
40
+ }
41
+ export declare class AuthorityClient {
42
+ private readonly channel;
43
+ private readonly store;
44
+ private readonly limit;
45
+ private readonly pending;
46
+ private closed;
47
+ epoch: string | undefined;
48
+ private constructor();
49
+ static connect(endpoint: string, store: string, limit: number, onChange: (snapshot: Snapshot) => void, disconnected: () => void): Promise<AuthorityClient>;
50
+ request(method: Method, args: readonly unknown[]): Promise<unknown>;
51
+ close(): void;
52
+ }
53
+ export declare class AuthorityServer {
54
+ private readonly store;
55
+ private readonly epoch;
56
+ private readonly limit;
57
+ private readonly dispatch;
58
+ private listener;
59
+ private readonly peers;
60
+ private readonly readyPeers;
61
+ private closed;
62
+ private inFlight;
63
+ private constructor();
64
+ static listen(endpoint: string, store: string, epoch: string, limit: number, dispatch: (method: Method, args: unknown[]) => Promise<unknown>): Promise<AuthorityServer>;
65
+ private accept;
66
+ changed(snapshot: Snapshot): void;
67
+ close(): void;
68
+ }
69
+ export declare function parseArguments(method: Method, args: unknown[]): unknown[];
70
+ export {};
@@ -1,17 +1,19 @@
1
1
  import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
2
2
  import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { OpenInAppLaunchState } from './controller.ts';
4
+ import type { OpenInAppOutcome } from './outcome.ts';
4
5
  import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client';
5
6
  import { NS } from './locales.ts';
7
+ import { type OpenTargetReporter } from './OpenTargetButton.tsx';
6
8
  /** Browser operations and state shared by the Session header and file tree contributions. */
7
- export interface OpenInAppActionInjected {
9
+ export interface OpenInAppActionInjected extends OpenTargetReporter {
8
10
  hooks: {
9
11
  openInAppApps: ObservableSnapshot<readonly string[] | null>;
10
12
  openInAppChoice: ObservableSnapshot<string>;
11
13
  openInAppLaunch: ObservableSnapshot<OpenInAppLaunchState>;
12
14
  shortcuts: ObservableSnapshot<readonly ShortcutCatalogEntry[]>;
13
15
  };
14
- launch: (appId: string, path: string) => Promise<void>;
16
+ launch: (appId: string, path: string) => Promise<OpenInAppOutcome>;
15
17
  choose: (appId: string) => void;
16
18
  iconUrl: (appId: string) => string;
17
19
  }
@@ -21,7 +23,7 @@ export type OpenInAppActionProps = PropsLocale<typeof NS> & InjectFace<OpenInApp
21
23
  };
22
24
  /**
23
25
  * Adapt the installed directory catalog to the shared opening control.
24
- * @param props - displayed directory, installed catalog, and launch operations.
26
+ * @param props - displayed directory, installed catalog, launch operations, and the announcement seat.
25
27
  * @returns the shared control, or null without an eligible application.
26
28
  */
27
29
  export declare function OpenInAppAction(props: OpenInAppActionProps): React.JSX.Element | null;
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client';
3
3
  import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
4
- import type { OpenInAppPathFailure } from './open-path.ts';
4
+ import type { OpenInAppOutcome } from './outcome.ts';
5
5
  import type { NS } from './locales.ts';
6
6
  /** Application metadata supplied by either the directory catalog or a file association query. */
7
7
  export interface OpenTargetApplication {
@@ -18,8 +18,13 @@ export type OpenTargetOperation = {
18
18
  } | {
19
19
  readonly kind: 'reveal';
20
20
  };
21
+ /** Seat a contribution sends one settled gesture to for app-wide announcement. */
22
+ export interface OpenTargetReporter {
23
+ /** Report the settled outcome; `{ kind: 'skipped' }` announces nothing. */
24
+ readonly report: (outcome: OpenInAppOutcome) => void;
25
+ }
21
26
  /** Inputs shared by both target adapters and the file empty-state action. */
22
- export interface OpenTargetButtonProps {
27
+ export interface OpenTargetButtonProps extends OpenTargetReporter {
23
28
  readonly shortcut?: Pick<ShortcutCatalogEntry, 'keys' | 'aria'> | undefined;
24
29
  readonly kind: 'file' | 'directory';
25
30
  readonly applications: readonly OpenTargetApplication[];
@@ -29,24 +34,25 @@ export interface OpenTargetButtonProps {
29
34
  readonly loading?: boolean;
30
35
  readonly prominent?: boolean;
31
36
  readonly t: TranslateNS<typeof NS>;
32
- readonly execute: (operation: OpenTargetOperation) => Promise<OpenInAppPathFailure | null>;
37
+ /** Resolve every gesture to an outcome, including Host and transport failures; never reject. */
38
+ readonly execute: (operation: OpenTargetOperation) => Promise<OpenInAppOutcome>;
33
39
  readonly refresh?: () => void;
34
40
  }
35
41
  /**
36
- * Serialize gestures and announce their failures through the initiating control's toast.
37
- * @param execute - target adapter that returns the failure to announce, or null.
38
- * @param t - localized control copy.
39
- * @returns the pending state, feedback, and guarded action callback.
42
+ * Serialize gestures and report the outcome each one settles on to the app-wide
43
+ * announcer, which outlives this control and the panel holding it.
44
+ * @param execute - target adapter that settles on the gesture's outcome.
45
+ * @param report - announcement seat the settled outcome is sent to.
46
+ * @returns the pending state and the guarded action callback.
40
47
  */
41
- export declare function useOpenTargetGesture(execute: OpenTargetButtonProps['execute'], t: TranslateNS<typeof NS>): {
48
+ export declare function useOpenTargetGesture(execute: OpenTargetButtonProps['execute'], report: OpenTargetButtonProps['report']): {
42
49
  pending: boolean;
43
- toast: ReactNode;
44
50
  act: (operation: OpenTargetOperation) => void;
45
51
  };
46
52
  /**
47
53
  * Render identical split buttons for files and directories. File reveal always
48
54
  * stays last; it is the default only when no application is registered.
49
55
  * @param props - target applications, default selection, and operations.
50
- * @returns the control and its transient failure feedback.
56
+ * @returns the control alone: its settled outcomes are announced by the seat it reports to.
51
57
  */
52
58
  export declare function OpenTargetButton(props: OpenTargetButtonProps): ReactNode;
@@ -25,6 +25,7 @@
25
25
  import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
26
26
  import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client';
27
27
  import { OpenInAppAction, type OpenInAppActionProps } from './OpenInAppAction.tsx';
28
+ import { type OpenInAppToastSource } from './open-toast.tsx';
28
29
  import { NS } from './locales.ts';
29
30
  /**
30
31
  * The props `OpenInAppAction` consumes, minus the per-render `absolutePath`
@@ -42,5 +43,8 @@ export type OpenInAppAdapter = Omit<OpenInAppActionProps, 'absolutePath'>;
42
43
  export declare function createOpenInAppAdapter(deps: {
43
44
  readonly t: (key: string, params?: Record<string, unknown>) => string;
44
45
  readonly shortcuts?: ObservableSnapshot<readonly ShortcutCatalogEntry[]> | undefined;
45
- }): OpenInAppAdapter | undefined;
46
+ }): {
47
+ readonly adapter: OpenInAppAdapter;
48
+ readonly toast: OpenInAppToastSource;
49
+ } | undefined;
46
50
  export { OpenInAppAction, NS as OPEN_IN_APP_NS };
@@ -1,5 +1,6 @@
1
1
  /** Browser availability/choice state and the launch carrier for the split button. */
2
2
  import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
3
+ import type { OpenInAppOutcome } from './outcome.ts';
3
4
  /** Shared launch status for controls targeting the captured workspace path. */
4
5
  export interface OpenInAppLaunchState {
5
6
  readonly phase: 'idle' | 'busy' | 'error';
@@ -45,9 +46,11 @@ export declare class OpenInAppController {
45
46
  * @param appId - catalog id from the availability list.
46
47
  * @param path - the session's absolute workspace directory.
47
48
  * Concurrent gestures are ignored until the current Host request settles.
48
- * @returns after the host acknowledged the launch; rejects on any failure.
49
+ * @returns the acknowledged handoff, or `{ kind: 'skipped' }` while another
50
+ * launch holds the operation, or a named failure when the Host refuses or
51
+ * cannot be reached. Host and transport failures resolve without rejecting.
49
52
  */
50
- launch(appId: string, path: string): Promise<void>;
53
+ launch(appId: string, path: string): Promise<OpenInAppOutcome>;
51
54
  private run;
52
55
  }
53
56
  export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Fork-facing shim for `isMobileDevice` from
3
+ * `@deepseek-ai/dsh-client-ui-primitives`.
4
+ *
5
+ * The verbatim `open-toast.tsx` spells this specifier where upstream writes
6
+ * the primitives import (joined only in `0.2.2-alpha.2`); the
7
+ * release-dependent probe and fallback live in
8
+ * `src/compat/open-in-app-device.ts` — version-dependent code belongs only
9
+ * under `src/compat/`. See FORK.md.
10
+ */
11
+ export { isMobileDevice } from '../../compat/open-in-app-device.ts';
@@ -1,7 +1,7 @@
1
1
  /** Dictionary namespace owned by upstream's plugin (verbatim). */
2
2
  export declare const NS = "open-in-app";
3
3
  /** Key domain of the `open-in-app` namespace (verbatim union of upstream's zh keys). */
4
- export type OpenInAppKey = 'open.title' | 'path.appDefault' | 'path.appsError' | 'shortcut.busy' | 'shortcut.unavailable' | 'open.tooltip' | 'path.open' | 'path.more' | 'path.reveal' | 'path.openError' | 'path.revealError' | 'app.cursor' | 'app.vscode' | 'app.vscodeinsiders' | 'app.windsurf' | 'app.zed' | 'app.sublimetext' | 'app.xcode' | 'app.androidstudio' | 'app.intellij' | 'app.pycharm' | 'app.webstorm' | 'app.phpstorm' | 'app.goland' | 'app.rider' | 'app.rustrover' | 'app.fork' | 'app.sourcetree' | 'app.github' | 'app.tower' | 'app.gitkraken' | 'app.smartgit' | 'app.sublimemerge' | 'app.ghostty' | 'app.warp' | 'app.iterm' | 'app.kitty' | 'app.terminal' | 'app.windowsterminal' | 'app.gitbash' | 'app.gnometerminal' | 'app.konsole' | 'app.finder' | 'app.explorer' | 'app.filemanager';
4
+ export type OpenInAppKey = 'open.title' | 'path.appDefault' | 'path.appsError' | 'shortcut.busy' | 'shortcut.unavailable' | 'open.tooltip' | 'path.open' | 'path.more' | 'path.reveal' | 'path.opened' | 'path.revealed' | 'path.openError' | 'path.revealError' | 'app.cursor' | 'app.vscode' | 'app.vscodeinsiders' | 'app.windsurf' | 'app.zed' | 'app.sublimetext' | 'app.xcode' | 'app.androidstudio' | 'app.intellij' | 'app.pycharm' | 'app.webstorm' | 'app.phpstorm' | 'app.goland' | 'app.rider' | 'app.rustrover' | 'app.fork' | 'app.sourcetree' | 'app.github' | 'app.tower' | 'app.gitkraken' | 'app.smartgit' | 'app.sublimemerge' | 'app.ghostty' | 'app.warp' | 'app.iterm' | 'app.kitty' | 'app.terminal' | 'app.windowsterminal' | 'app.gitbash' | 'app.gnometerminal' | 'app.konsole' | 'app.finder' | 'app.explorer' | 'app.filemanager';
5
5
  declare module '@deepseek-ai/dsh-client-ui-slots' {
6
6
  interface LocaleNamespaceMap {
7
7
  /** Session-header "open workspace in application" copy (upstream declaration). */
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Fork-facing shim for upstream `ui-open-in-app/src/client/open-path.ts`.
3
3
  *
4
- * The fork only needs the failure union `OpenInAppPathButton`'s `execute`
4
+ * The fork only needs the failure union the path gestures' `execute`
5
5
  * returns; upstream's file also carries `OpenInAppPathController`, the
6
6
  * document-preview path controller that speaks to a Session Remote this
7
7
  * plugin does not hold. Keeping the verbatim file would pull
8
8
  * `@deepseek-ai/dsh-api-remotes` and `dsh-api-session-controller` into the
9
- * bundle for a class nothing here calls — so this shim owns just the type,
10
- * spelled verbatim. See FORK.md.
9
+ * bundle for a class nothing here calls. From `0.2.2-alpha.2` the failure
10
+ * union lives in upstream's `outcome.ts` (an alias of the deliverables
11
+ * `PresentedOpenFailure`), so this shim re-exports the fork's own outcome
12
+ * shim under the same name. See FORK.md.
11
13
  */
12
- /** Failure kind of one settled path gesture, for the initiating control to announce. */
13
- export type OpenInAppPathFailure = 'openError' | 'revealError';
14
+ export type { OpenInAppPathFailure } from './outcome.ts';
@@ -0,0 +1,39 @@
1
+ /** Native-open feedback that survives the initiating panel: mobile success and all failures. */
2
+ import type { ReactNode } from 'react';
3
+ import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
4
+ import { NS } from './locales.ts';
5
+ import type { OpenInAppOutcome } from './outcome.ts';
6
+ /** One announced gesture; `seq` keys the banner so a repeat restarts it. */
7
+ export type OpenInAppToastState = Exclude<OpenInAppOutcome, {
8
+ kind: 'skipped';
9
+ }> & {
10
+ readonly seq: number;
11
+ };
12
+ /** Store every contribution reports into and the overlay entry reads. */
13
+ export interface OpenInAppToastSource {
14
+ /** Current notice for the overlay entry, or null while none shows. */
15
+ readonly hooks: {
16
+ readonly toast: HostObservable<OpenInAppToastState | null>;
17
+ };
18
+ /**
19
+ * Announce failures on every device and acknowledged opens or reveals on mobile.
20
+ * Desktop successes and skipped gestures leave the current notice unchanged.
21
+ * @param outcome - settled native-open result.
22
+ */
23
+ readonly report: (outcome: OpenInAppOutcome) => void;
24
+ /** Clear the shown notice. */
25
+ readonly dismiss: () => void;
26
+ }
27
+ /**
28
+ * Create the one outcome store the overlay entry shows.
29
+ * @returns the observable notice with its report and dismiss actions.
30
+ */
31
+ export declare function createOpenInAppToastSource(): OpenInAppToastSource;
32
+ /** Props of the `shell.overlay` entry: the notice store, its dismissal, and localized copy. */
33
+ export type OpenInAppToastProps = PropsRuntime<'shell.overlay'> & PropsLocale<typeof NS> & InjectFace<Omit<OpenInAppToastSource, 'report'>>;
34
+ /**
35
+ * Render a mobile handoff confirmation or a native-open failure.
36
+ * @param props - the notice hook, its dismissal, and the locale seat.
37
+ * @returns the banner on display, or null.
38
+ */
39
+ export declare function OpenInAppToast({ useToast, dismiss, t }: OpenInAppToastProps): ReactNode;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Fork-facing shim for upstream `ui-open-in-app/src/client/outcome.ts`.
3
+ *
4
+ * Upstream's file aliases `PresentedOpenFailure`/`PresentedOpenOutcome` out
5
+ * of `@deepseek-ai/dsh-client-ui-deliverables` — a package this plugin does
6
+ * not depend on. Both aliases are types only, so this shim spells the same
7
+ * structural unions verbatim (upstream
8
+ * `ui-deliverables/src/client/present-open.ts`) instead of importing them.
9
+ * See FORK.md.
10
+ */
11
+ /** Failure kind of one settled gesture, keyed to the `path.*` copy it announces. */
12
+ export type OpenInAppPathFailure = 'openError' | 'revealError';
13
+ /** Acknowledged open or reveal, named failure, or deliberately skipped gesture. */
14
+ export type OpenInAppOutcome = {
15
+ readonly kind: 'opened';
16
+ } | {
17
+ readonly kind: 'revealed';
18
+ } | {
19
+ readonly kind: 'failed';
20
+ readonly failure: OpenInAppPathFailure;
21
+ } | {
22
+ readonly kind: 'skipped';
23
+ };