@khorsheed/dsh-game 0.0.0-stage → 0.1.1

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 (101) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.en.md +53 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +52 -2
  5. package/cordis.patch.yml +23 -0
  6. package/lib/client.js +30690 -0
  7. package/lib/index.js +4297 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3876 -0
  10. package/lib/typert.remote-client.d.ts +104 -0
  11. package/lib/typert.remote-client.js +2138 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +43 -0
  14. package/lib/types/checks.d.ts +44 -0
  15. package/lib/types/checks.js +267 -0
  16. package/lib/types/client/DesignView.d.ts +22 -0
  17. package/lib/types/client/DesignView.js +134 -0
  18. package/lib/types/client/ElementsView.d.ts +21 -0
  19. package/lib/types/client/ElementsView.js +195 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +261 -0
  22. package/lib/types/client/PlayFrame.d.ts +54 -0
  23. package/lib/types/client/PlayFrame.js +129 -0
  24. package/lib/types/client/PlayView.d.ts +19 -0
  25. package/lib/types/client/PlayView.js +163 -0
  26. package/lib/types/client/Preview3D.d.ts +52 -0
  27. package/lib/types/client/Preview3D.js +178 -0
  28. package/lib/types/client/ProjectSwitcher.d.ts +20 -0
  29. package/lib/types/client/ProjectSwitcher.js +75 -0
  30. package/lib/types/client/Recipe3DEditor.d.ts +19 -0
  31. package/lib/types/client/Recipe3DEditor.js +44 -0
  32. package/lib/types/client/StyleGallery.d.ts +18 -0
  33. package/lib/types/client/StyleGallery.js +27 -0
  34. package/lib/types/client/TurnCard.d.ts +62 -0
  35. package/lib/types/client/TurnCard.js +100 -0
  36. package/lib/types/client/VersionsView.d.ts +17 -0
  37. package/lib/types/client/VersionsView.js +68 -0
  38. package/lib/types/client/chat-node.d.ts +42 -0
  39. package/lib/types/client/chat-node.js +77 -0
  40. package/lib/types/client/contract.d.ts +131 -0
  41. package/lib/types/client/contract.js +6 -0
  42. package/lib/types/client/definition.d.ts +22 -0
  43. package/lib/types/client/definition.js +28 -0
  44. package/lib/types/client/index.d.ts +21 -0
  45. package/lib/types/client/index.js +263 -0
  46. package/lib/types/client/locales.d.ts +379 -0
  47. package/lib/types/client/locales.js +733 -0
  48. package/lib/types/client/looks.d.ts +36 -0
  49. package/lib/types/client/looks.js +105 -0
  50. package/lib/types/client/markdown.d.ts +11 -0
  51. package/lib/types/client/markdown.js +52 -0
  52. package/lib/types/client/preset-visibility.d.ts +94 -0
  53. package/lib/types/client/preset-visibility.js +115 -0
  54. package/lib/types/client/preview-entry.d.ts +24 -0
  55. package/lib/types/client/preview-entry.js +110 -0
  56. package/lib/types/client/refs.d.ts +36 -0
  57. package/lib/types/client/refs.js +44 -0
  58. package/lib/types/client/reveal.d.ts +43 -0
  59. package/lib/types/client/reveal.js +73 -0
  60. package/lib/types/client/style-samples.d.ts +5 -0
  61. package/lib/types/client/style-samples.js +196 -0
  62. package/lib/types/fsutil.d.ts +26 -0
  63. package/lib/types/fsutil.js +104 -0
  64. package/lib/types/hooks.d.ts +83 -0
  65. package/lib/types/hooks.js +108 -0
  66. package/lib/types/index.d.ts +30 -0
  67. package/lib/types/index.js +22 -0
  68. package/lib/types/kits/canvas2d.d.ts +6 -0
  69. package/lib/types/kits/canvas2d.js +60 -0
  70. package/lib/types/kits/ids.d.ts +11 -0
  71. package/lib/types/kits/ids.js +6 -0
  72. package/lib/types/kits/index.d.ts +26 -0
  73. package/lib/types/kits/index.js +28 -0
  74. package/lib/types/kits/three3d.d.ts +12 -0
  75. package/lib/types/kits/three3d.js +188 -0
  76. package/lib/types/kits/types.d.ts +52 -0
  77. package/lib/types/kits/types.js +6 -0
  78. package/lib/types/look3d.d.ts +20 -0
  79. package/lib/types/look3d.js +175 -0
  80. package/lib/types/panel-session.d.ts +21 -0
  81. package/lib/types/panel-session.js +16 -0
  82. package/lib/types/project.d.ts +138 -0
  83. package/lib/types/project.js +504 -0
  84. package/lib/types/remote.d.ts +184 -0
  85. package/lib/types/remote.js +263 -0
  86. package/lib/types/skills.d.ts +13 -0
  87. package/lib/types/skills.js +141 -0
  88. package/lib/types/store.d.ts +346 -0
  89. package/lib/types/store.js +1383 -0
  90. package/lib/types/tools.d.ts +12 -0
  91. package/lib/types/tools.js +309 -0
  92. package/lib/types/trash.d.ts +5 -0
  93. package/lib/types/trash.js +64 -0
  94. package/lib/types/types.d.ts +548 -0
  95. package/lib/types/types.js +85 -0
  96. package/lib/types/vfs.d.ts +94 -0
  97. package/lib/types/vfs.js +217 -0
  98. package/locale/en.json +6 -0
  99. package/locale/zh.json +6 -0
  100. package/package.json +132 -3
  101. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The game tab's injected face and props.
3
+ * @module @khorsheed/dsh-game/client
4
+ */
5
+ import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
6
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
7
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
8
+ import type { Reveal } from './reveal.ts';
9
+ import type { BundleFile, LiveVersion, CommitResult, DeleteProjectResult, WorkingChanges, GameResult, PlayBundle, ProjectCard, SessionGameState, TurnReport, VersionDiff } from '../types.ts';
10
+ type R<T> = Promise<RemoteResult<T>>;
11
+ type Ok<T extends object = object> = GameResult<{
12
+ readonly ok: true;
13
+ } & T>;
14
+ /** Every call the panel makes, already bound to the Remote. */
15
+ export interface GameApi {
16
+ state: (sid: SessionId) => R<SessionGameState>;
17
+ liveVersion: (sid: SessionId) => R<LiveVersion | null>;
18
+ projectCards: () => R<ProjectCard[]>;
19
+ turnReport: (sid: SessionId, turn: number) => R<TurnReport | null>;
20
+ bind: (sid: SessionId, projectId: string | null) => R<Ok>;
21
+ deleteProject: (sid: SessionId, projectId: string, mode: 'trash' | 'forget') => R<DeleteProjectResult>;
22
+ bundle: (projectId: string, which: 'current' | 'candidate') => R<Ok<{
23
+ readonly bundle: PlayBundle;
24
+ }>>;
25
+ versionBundle: (projectId: string, version: number) => R<Ok<{
26
+ readonly bundle: PlayBundle;
27
+ }>>;
28
+ workingFile: (projectId: string, path: string) => R<Ok<{
29
+ readonly file: BundleFile;
30
+ }>>;
31
+ confirm: (sid: SessionId, projectId: string) => R<Ok<{
32
+ readonly version: number;
33
+ }>>;
34
+ commit: (sid: SessionId, projectId: string) => R<GameResult<CommitResult>>;
35
+ changes: (projectId: string) => R<GameResult<WorkingChanges & {
36
+ readonly ok: true;
37
+ }>>;
38
+ reject: (sid: SessionId, projectId: string, reason: string) => R<Ok>;
39
+ rollback: (sid: SessionId, projectId: string, version: number) => R<Ok>;
40
+ diff: (projectId: string, from: number, to: number) => R<Ok<{
41
+ readonly diff: VersionDiff;
42
+ }>>;
43
+ checkShot: (projectId: string, version?: number) => R<Ok<{
44
+ readonly webp: string;
45
+ }>>;
46
+ setAutoConfirm: (sid: SessionId, projectId: string, on: boolean) => R<Ok>;
47
+ logPlay: (sid: SessionId, entry: {
48
+ projectId: string;
49
+ version: number;
50
+ seconds: number;
51
+ errors: string[];
52
+ }) => R<Ok>;
53
+ setParam: (sid: SessionId, projectId: string, key: string, value: number) => R<Ok>;
54
+ finalizeParams: (sid: SessionId, projectId: string) => R<Ok>;
55
+ resetParams: (sid: SessionId, projectId: string) => R<Ok>;
56
+ editElement: (sid: SessionId, projectId: string, elementId: string, patch: {
57
+ look?: string;
58
+ fields?: string;
59
+ }) => R<Ok<{
60
+ readonly elementJson: string;
61
+ }>>;
62
+ pickProposal: (sid: SessionId, projectId: string, elementId: string, at: string, index: number) => R<Ok>;
63
+ writeChapter: (sid: SessionId, projectId: string, chapterId: string, text: string) => R<Ok>;
64
+ requestRestyle: (sid: SessionId, projectId: string, styleId: string) => R<Ok>;
65
+ setOnePager: (sid: SessionId, projectId: string, patch: {
66
+ pitch?: string;
67
+ elementTypes?: string;
68
+ pillars?: string;
69
+ }) => R<Ok>;
70
+ dropPending: (sid: SessionId, projectId: string, key: string) => R<Ok>;
71
+ }
72
+ /** Where a jump into the game tab lands. */
73
+ export interface NavTarget {
74
+ readonly view: 'play' | 'elements' | 'design' | 'versions';
75
+ readonly elementId?: string;
76
+ readonly chapterId?: string;
77
+ }
78
+ /** What the tab body gets from the plugin. */
79
+ export interface GameTabInjected {
80
+ /** Open the game tab at a place (the turn card's 查看). */
81
+ openAt: (target: NavTarget) => void;
82
+ /** The tab body follows jumps; returns the unsubscribe. */
83
+ subscribeNav: (listener: (target: NavTarget) => void) => () => void;
84
+ /** A jump made before the tab body mounted, taken once. */
85
+ takeNav: () => NavTarget | null;
86
+ api: GameApi;
87
+ /** Put text into the session's composer draft (appended; never sent). False when there is no composer. */
88
+ insertDraft: (sessionId: SessionId, text: string) => boolean;
89
+ /**
90
+ * Put a reference into the session's composer as one chip (`label` shown,
91
+ * `text` what the model receives); falls back to appending `text` as plain
92
+ * draft text where the host has no chip seam. Never sends.
93
+ */
94
+ insertRef: (sessionId: SessionId, ref: {
95
+ readonly label: string;
96
+ readonly text: string;
97
+ }) => boolean;
98
+ /** Turn-boundary pushes, plus the panel's own mutations, per session id. */
99
+ subscribeSessionEvents: (listener: (sessionId: string) => void) => () => void;
100
+ /** Tell every game surface of this page that a session's state moved. */
101
+ touch: (sessionId: SessionId) => void;
102
+ /** Carries out the agent's 「请用户看这里」 (page-wide). */
103
+ reveal: Reveal;
104
+ }
105
+ /** Full props of the game tab body. */
106
+ export type GameTabProps = PropsRuntime<'sidebar.right.pane.tab'> & InjectFace<GameTabInjected> & PropsLocale<'game'>;
107
+ /** What every view inside the tab receives. */
108
+ export interface ViewProps {
109
+ readonly t: GameTabProps['t'];
110
+ readonly sessionId: GameTabProps['sessionId'];
111
+ readonly api: GameApi;
112
+ readonly insertDraft: (text: string) => boolean;
113
+ /** Insert a reference chip into the composer (see `GameTabInjected.insertRef`). */
114
+ readonly insertRef: (ref: {
115
+ readonly label: string;
116
+ readonly text: string;
117
+ }) => boolean;
118
+ /** Re-read the state after a mutation (and tell the other game surfaces). */
119
+ readonly refresh: () => void;
120
+ /** Show a short confirmation line at the panel's foot. */
121
+ readonly toast: (text: string) => void;
122
+ /** Carries out the agent's 「请用户看这里」 (page-wide). */
123
+ readonly reveal: Reveal;
124
+ }
125
+ /** A Remote call's game-level failure code, or undefined on success. */
126
+ export declare function failureOf(result: RemoteResult<{
127
+ ok: boolean;
128
+ error?: string;
129
+ }>): string | undefined;
130
+ export {};
131
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,6 @@
1
+ /** A Remote call's game-level failure code, or undefined on success. */
2
+ export function failureOf(result) {
3
+ if (!result.ok)
4
+ return result.error.message;
5
+ return result.value.ok ? undefined : result.value.error ?? 'io';
6
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stage one of the right-Sidebar registration: what the `game` tab type IS —
3
+ * a page (no address claims), opened from the guide or by kind.
4
+ */
5
+ import type { SidebarRightTabDefinition } from '@deepseek-ai/dsh-client-ui-sidebar-right/client';
6
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
7
+ /** The tab kind this package owns. */
8
+ export declare const GAME_KIND = "game";
9
+ /** This implementation's identity in the tab system, and the key its body registers under. */
10
+ export declare const GAME_TAB_ID = "@khorsheed/dsh-game";
11
+ /** The guide glyph: a gamepad on a tinted tile, in currentColor. */
12
+ export declare function GameGlyph({ size, className }: {
13
+ size?: number | undefined;
14
+ className?: string | undefined;
15
+ }): import("react").JSX.Element;
16
+ /**
17
+ * The type's registry definition (default `extension` band).
18
+ * @param t - namespace-bound translate.
19
+ * @returns the definition to register.
20
+ */
21
+ export declare function gameDefinition(t: TranslateNS<'game'>): SidebarRightTabDefinition;
22
+ //# sourceMappingURL=definition.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** The tab kind this package owns. */
3
+ export const GAME_KIND = 'game';
4
+ /** This implementation's identity in the tab system, and the key its body registers under. */
5
+ export const GAME_TAB_ID = '@khorsheed/dsh-game';
6
+ /** The guide glyph: a gamepad on a tinted tile, in currentColor. */
7
+ export function GameGlyph({ size = 16, className }) {
8
+ return (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("rect", { width: "16", height: "16", rx: "4", fill: "currentColor", fillOpacity: "0.1" }), _jsx("path", { d: "M5.2 5.5h5.6a2.6 2.6 0 0 1 2.5 1.9l.6 2.3a1.4 1.4 0 0 1-2.3 1.4L10.4 10H5.6l-1.2 1.1a1.4 1.4 0 0 1-2.3-1.4l.6-2.3a2.6 2.6 0 0 1 2.5-1.9Z", stroke: "currentColor", strokeWidth: "1.1", strokeLinejoin: "round" }), _jsx("path", { d: "M5.4 7v1.6M4.6 7.8h1.6", stroke: "currentColor", strokeWidth: "1.1", strokeLinecap: "round" }), _jsx("circle", { cx: "10.4", cy: "7.4", r: ".6", fill: "currentColor" }), _jsx("circle", { cx: "11.4", cy: "8.4", r: ".6", fill: "currentColor" })] }));
9
+ }
10
+ /**
11
+ * The type's registry definition (default `extension` band).
12
+ * @param t - namespace-bound translate.
13
+ * @returns the definition to register.
14
+ */
15
+ export function gameDefinition(t) {
16
+ return {
17
+ id: GAME_TAB_ID,
18
+ kind: GAME_KIND,
19
+ title: () => t('tab.title'),
20
+ guide: [{
21
+ id: GAME_KIND,
22
+ order: 55,
23
+ title: () => t('guide.title'),
24
+ description: () => t('guide.description'),
25
+ icon: GameGlyph,
26
+ }],
27
+ };
28
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Game mode, browser half: mounts the `game` Remote, registers the `game`
3
+ * right-Sidebar tab type (self-hidden where the session cannot reach the game
4
+ * tools) and its body in the keyed pane seat. Turn-boundary pushes
5
+ * (`api-session/status` / `api-session/activity`, the two the official
6
+ * allowlist forwards) refresh the tab, which is how a version deployed at the
7
+ * end of a turn reaches the play area.
8
+ * @module @khorsheed/dsh-game/client
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import { GAME_KIND, GAME_TAB_ID } from './definition.tsx';
12
+ import { GameTab } from './GameTab.tsx';
13
+ export { GameTab, GAME_KIND, GAME_TAB_ID };
14
+ /** `remote.game` is mounted here and read back through ctx.get (never injected — that would deadlock the loader). */
15
+ export declare const inject: string[];
16
+ /**
17
+ * Client plugin body.
18
+ * @param ctx - client root context.
19
+ */
20
+ export declare function apply(ctx: Context): Promise<() => Promise<void>>;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,263 @@
1
+ import gameRemote from '@khorsheed/dsh-game/remote';
2
+ import { GAME_CHAT_KIND, GameChatRow, gameMessageDefinition } from "./chat-node.js";
3
+ import { gameTurnDefinition, GameTurnCard } from "./TurnCard.js";
4
+ import { Reveal } from "./reveal.js";
5
+ import { encodeGameRef, GAME_REF_SOURCE, gameRefCodec } from "./refs.js";
6
+ import { GAME_KIND, GAME_TAB_ID, gameDefinition } from "./definition.js";
7
+ import { GameTab } from "./GameTab.js";
8
+ import { en, NS, zh } from "./locales.js";
9
+ import { GameTabVisibility, RegistrationToggle } from "./preset-visibility.js";
10
+ export { GameTab, GAME_KIND, GAME_TAB_ID };
11
+ function mainSessionId(list) {
12
+ const view = list;
13
+ return Object.values(view.byId).find(s => (s.retainedBy?.mainView ?? 0) > 0)?.id ?? view.current;
14
+ }
15
+ /** `remote.game` is mounted here and read back through ctx.get (never injected — that would deadlock the loader). */
16
+ export const inject = ['slots', 'remote', 'locale', 'sessions', 'sidebarRight', 'sidebarRightTabs'];
17
+ /**
18
+ * Client plugin body.
19
+ * @param ctx - client root context.
20
+ */
21
+ export async function apply(ctx) {
22
+ const disposers = [];
23
+ try {
24
+ disposers.push(await ctx.remote.$mount(gameRemote));
25
+ }
26
+ catch (error) {
27
+ /* v8 ignore next -- double-mount is a composition error, not a runtime path */
28
+ ctx.logger.error(error);
29
+ }
30
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'game: dictionaries');
31
+ const t = ctx.locale.bind(NS);
32
+ const remote = ctx.get('remote.game');
33
+ const pluginInventory = ctx.get('remote.pluginInventory');
34
+ const listeners = new Set();
35
+ const emit = (sessionId) => { for (const listener of listeners)
36
+ listener(sessionId); };
37
+ const carrier = ctx.remote;
38
+ if (typeof carrier.$on === 'function') {
39
+ for (const forwarded of ['api-session/status', 'api-session/activity']) {
40
+ const off = carrier.$on(forwarded, (sessionId) => { emit(sessionId); });
41
+ disposers.push(async () => { off(); });
42
+ }
43
+ }
44
+ const tabVisibility = new GameTabVisibility(ctx, pluginInventory);
45
+ ctx.effect(() => {
46
+ const toggle = new RegistrationToggle(() => ctx.sidebarRightTabs.register(gameDefinition(t)), () => tabVisibility.show(mainSessionId(ctx.sessions.list.getSnapshot())));
47
+ toggle.setReady(true);
48
+ const unsubscribe = tabVisibility.subscribe(() => { toggle.sync(); });
49
+ return () => { unsubscribe(); toggle.setReady(false); };
50
+ }, 'game: tab type visibility');
51
+ const api = {
52
+ state: sid => remote.state(sid),
53
+ liveVersion: sid => remote.liveVersion(sid),
54
+ projectCards: () => remote.projectCards(),
55
+ turnReport: (sid, turn) => remote.turnReport(sid, { turn }),
56
+ bind: (sid, projectId) => remote.bind(sid, { projectId }),
57
+ deleteProject: (sid, projectId, mode) => remote.deleteProject(sid, { projectId, mode }),
58
+ bundle: (projectId, which) => remote.bundle({ projectId, which }),
59
+ versionBundle: (projectId, version) => remote.versionBundle({ projectId, version }),
60
+ workingFile: (projectId, path) => remote.workingFile({ projectId, path }),
61
+ confirm: (sid, projectId) => remote.confirm(sid, { projectId }),
62
+ commit: (sid, projectId) => remote.commit(sid, { projectId }),
63
+ changes: projectId => remote.changes({ projectId }),
64
+ reject: (sid, projectId, reason) => remote.reject(sid, { projectId, reason }),
65
+ rollback: (sid, projectId, version) => remote.rollback(sid, { projectId, version }),
66
+ diff: (projectId, from, to) => remote.diff({ projectId, from, to }),
67
+ checkShot: (projectId, version) => remote.checkShot({ projectId, ...(version === undefined ? {} : { version }) }),
68
+ setAutoConfirm: (sid, projectId, on) => remote.setAutoConfirm(sid, { projectId, on }),
69
+ logPlay: (sid, entry) => remote.logPlay(sid, entry),
70
+ setParam: (sid, projectId, key, value) => remote.setParam(sid, { projectId, key, value }),
71
+ finalizeParams: (sid, projectId) => remote.finalizeParams(sid, { projectId }),
72
+ resetParams: (sid, projectId) => remote.resetParams(sid, { projectId }),
73
+ editElement: (sid, projectId, elementId, patch) => remote.editElement(sid, { projectId, elementId, ...patch }),
74
+ pickProposal: (sid, projectId, elementId, at, index) => remote.pickProposal(sid, { projectId, elementId, at, index }),
75
+ writeChapter: (sid, projectId, chapterId, text) => remote.writeChapter(sid, { projectId, chapterId, text }),
76
+ requestRestyle: (sid, projectId, styleId) => remote.requestRestyle(sid, { projectId, styleId }),
77
+ setOnePager: (sid, projectId, patch) => remote.setOnePager(sid, { projectId, ...patch }),
78
+ dropPending: (sid, projectId, key) => remote.dropPending(sid, { projectId, key }),
79
+ };
80
+ // Reference chips (the quote precedent): a headless trigger source carries
81
+ // the codec the submit-time serializer resolves by source name; the chip
82
+ // itself rides the scoped `slash/input-insert-reference` bail event. The
83
+ // source registers on a deferred inject arm (mount order is unconstrained);
84
+ // without the service, or when the host declines, the reference falls back
85
+ // to plain draft text.
86
+ let chipsReady = false;
87
+ ctx.inject(['inputTriggers'], (lctx) => {
88
+ lctx.effect(() => {
89
+ try {
90
+ const service = lctx.get('inputTriggers');
91
+ if (service === undefined)
92
+ return () => { };
93
+ const dispose = service.registerSource({
94
+ trigger: '@',
95
+ name: GAME_REF_SOURCE,
96
+ candidates: async () => [],
97
+ onPick: () => undefined,
98
+ codec: gameRefCodec,
99
+ openReference: () => false,
100
+ });
101
+ chipsReady = true;
102
+ return () => { chipsReady = false; dispose(); };
103
+ }
104
+ catch (error) {
105
+ ctx.logger.error(error);
106
+ return () => { };
107
+ }
108
+ }, 'game: reference chips');
109
+ });
110
+ const inputFor = (sessionId) => {
111
+ try {
112
+ const scope = ctx.get('sessions')?.scope?.(sessionId);
113
+ const input = scope?.get('conversation')?.input?.for(scope);
114
+ const face = input;
115
+ return typeof face?.setDraft === 'function' && typeof face.state?.getSnapshot === 'function' ? face : undefined;
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ };
121
+ const navListeners = new Set();
122
+ let parkedNav = null;
123
+ const reveal = new Reveal(target => { face.openAt(target); });
124
+ const face = {
125
+ openAt: (target) => {
126
+ parkedNav = target;
127
+ try {
128
+ ctx.sidebarRight.openTab(GAME_KIND);
129
+ }
130
+ catch (error) {
131
+ ctx.logger.warn('game: openTab failed (tab type not registered?)', error);
132
+ }
133
+ for (const listener of navListeners)
134
+ listener(target);
135
+ },
136
+ subscribeNav: (listener) => {
137
+ navListeners.add(listener);
138
+ return () => { navListeners.delete(listener); };
139
+ },
140
+ takeNav: () => {
141
+ const target = parkedNav;
142
+ parkedNav = null;
143
+ return target;
144
+ },
145
+ api,
146
+ insertDraft: (sessionId, text) => {
147
+ const input = inputFor(sessionId);
148
+ if (input === undefined)
149
+ return false;
150
+ const kept = input.state.getSnapshot().draft.replace(/\s+$/, '');
151
+ input.setDraft(kept.length === 0 ? text : `${kept}\n\n${text}`);
152
+ if (typeof input.focus === 'function')
153
+ input.focus();
154
+ return true;
155
+ },
156
+ insertRef: (sessionId, ref) => {
157
+ const scope = ctx.get('sessions')?.scope?.(sessionId);
158
+ const input = inputFor(sessionId);
159
+ if (input === undefined)
160
+ return false;
161
+ if (chipsReady && scope?.bail !== undefined) {
162
+ for (let attempt = 0; attempt < 2; attempt += 1) {
163
+ const snapshot = input.state.getSnapshot();
164
+ if (typeof snapshot.draftRev !== 'number')
165
+ break;
166
+ // The span speaks the detect projection: each existing chip folds to one character.
167
+ const folded = (snapshot.occurrences ?? []).reduce((extra, o) => extra + o.length - 1, 0);
168
+ const end = snapshot.draft.length - folded;
169
+ const applied = scope.bail(scope, 'slash/input-insert-reference', {
170
+ reference: { source: GAME_REF_SOURCE, ref: encodeGameRef(ref), label: ref.label, clipboardText: ref.text },
171
+ span: { start: end, end, draftRev: snapshot.draftRev },
172
+ }) === true;
173
+ if (applied) {
174
+ input.focus?.();
175
+ return true;
176
+ }
177
+ }
178
+ }
179
+ const kept = input.state.getSnapshot().draft.replace(/\s+$/, '');
180
+ input.setDraft(kept.length === 0 ? `${ref.text} ` : `${kept} ${ref.text} `);
181
+ input.focus?.();
182
+ return true;
183
+ },
184
+ subscribeSessionEvents: (listener) => {
185
+ listeners.add(listener);
186
+ return () => { listeners.delete(listener); };
187
+ },
188
+ touch: sessionId => { emit(sessionId); },
189
+ reveal,
190
+ };
191
+ // The page follows the agent: game_publish leaves a 「请用户看这里」 request the host cannot push,
192
+ // so the page polls the on-screen session's bound project while visible (session events nudge
193
+ // it early). The same read notices a binding made with no event (a room member's game_open) and
194
+ // refreshes the tab — refresh only; where to go is the agent's to say.
195
+ const lastBound = new Map();
196
+ let lastLook = 0;
197
+ const look = () => {
198
+ const now = Date.now();
199
+ if (now - lastLook < 3000)
200
+ return;
201
+ lastLook = now;
202
+ const sid = mainSessionId(ctx.sessions.list.getSnapshot());
203
+ if (sid === undefined)
204
+ return;
205
+ void api.liveVersion(sid).then(r => {
206
+ if (!r.ok)
207
+ return;
208
+ const bound = r.value?.projectId ?? null;
209
+ const before = lastBound.get(sid);
210
+ lastBound.set(sid, bound);
211
+ if (before !== undefined && before !== bound)
212
+ emit(sid);
213
+ if (r.value?.show !== undefined)
214
+ reveal.request(r.value.projectId, r.value.show);
215
+ }).catch(() => { });
216
+ };
217
+ listeners.add(() => { look(); });
218
+ const pollTimer = setInterval(() => { if (document.visibilityState === 'visible')
219
+ look(); }, 6000);
220
+ disposers.push(async () => { clearInterval(pollTimer); });
221
+ ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
222
+ name: 'sidebar.right.pane.tab',
223
+ key: GAME_TAB_ID,
224
+ locale: NS,
225
+ inject: () => face,
226
+ }, GameTab)), 'game: tab body');
227
+ // The turn card: what each agent turn changed in the game, beneath the
228
+ // turn's closing prose (outside the fold), with jumps into the tab. The
229
+ // canvas-novel turn-tail precedent.
230
+ // The seat's owner props are typed by ui-chat; this card reads them
231
+ // structurally (the canvas-novel precedent), so it registers structurally.
232
+ const seats = ctx.slots;
233
+ ctx.effect(() => seats.inject('conversation.chat.turnTail', () => {
234
+ const conversation = ctx.get.bind(ctx)('uiConversation');
235
+ const register = conversation?.events?.register;
236
+ if (register === undefined)
237
+ return () => undefined;
238
+ const disposeDefinition = register.call(conversation?.events, gameTurnDefinition);
239
+ const disposeEntry = seats.register({
240
+ name: 'conversation.chat.turnTail',
241
+ id: '@khorsheed/dsh-game/turn',
242
+ order: 45,
243
+ locale: NS,
244
+ inject: () => face,
245
+ }, GameTurnCard);
246
+ return () => {
247
+ disposeEntry();
248
+ disposeDefinition();
249
+ };
250
+ }), 'game: turn-tail card');
251
+ // The conversation rows for dsh-game messages (check findings, attached changes).
252
+ ctx.inject(['uiConversation'], (lctx) => {
253
+ lctx.effect(() => lctx.uiConversation.events.register(gameMessageDefinition), 'game: chat projection');
254
+ });
255
+ ctx.effect(() => ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
256
+ name: 'conversation.chat.node',
257
+ key: GAME_CHAT_KIND,
258
+ locale: NS,
259
+ }, GameChatRow)), 'game: chat row');
260
+ return async () => {
261
+ await Promise.all(disposers.map(dispose => dispose()));
262
+ };
263
+ }