@khorsheed/dsh-game 0.0.0-stage → 0.1.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +17 -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 +30348 -0
  7. package/lib/index.js +4154 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3769 -0
  10. package/lib/typert.remote-client.d.ts +101 -0
  11. package/lib/typert.remote-client.js +2082 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +42 -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 +187 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +256 -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 +161 -0
  26. package/lib/types/client/Preview3D.d.ts +50 -0
  27. package/lib/types/client/Preview3D.js +176 -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 +125 -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 +229 -0
  46. package/lib/types/client/locales.d.ts +376 -0
  47. package/lib/types/client/locales.js +727 -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 +107 -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/style-samples.d.ts +5 -0
  59. package/lib/types/client/style-samples.js +196 -0
  60. package/lib/types/fsutil.d.ts +26 -0
  61. package/lib/types/fsutil.js +104 -0
  62. package/lib/types/hooks.d.ts +83 -0
  63. package/lib/types/hooks.js +108 -0
  64. package/lib/types/index.d.ts +30 -0
  65. package/lib/types/index.js +22 -0
  66. package/lib/types/kits/canvas2d.d.ts +6 -0
  67. package/lib/types/kits/canvas2d.js +60 -0
  68. package/lib/types/kits/ids.d.ts +11 -0
  69. package/lib/types/kits/ids.js +6 -0
  70. package/lib/types/kits/index.d.ts +26 -0
  71. package/lib/types/kits/index.js +28 -0
  72. package/lib/types/kits/three3d.d.ts +12 -0
  73. package/lib/types/kits/three3d.js +188 -0
  74. package/lib/types/kits/types.d.ts +52 -0
  75. package/lib/types/kits/types.js +6 -0
  76. package/lib/types/look3d.d.ts +20 -0
  77. package/lib/types/look3d.js +175 -0
  78. package/lib/types/panel-session.d.ts +21 -0
  79. package/lib/types/panel-session.js +16 -0
  80. package/lib/types/project.d.ts +132 -0
  81. package/lib/types/project.js +494 -0
  82. package/lib/types/remote.d.ts +182 -0
  83. package/lib/types/remote.js +256 -0
  84. package/lib/types/skills.d.ts +13 -0
  85. package/lib/types/skills.js +140 -0
  86. package/lib/types/store.d.ts +325 -0
  87. package/lib/types/store.js +1285 -0
  88. package/lib/types/tools.d.ts +12 -0
  89. package/lib/types/tools.js +301 -0
  90. package/lib/types/trash.d.ts +5 -0
  91. package/lib/types/trash.js +64 -0
  92. package/lib/types/types.d.ts +522 -0
  93. package/lib/types/types.js +85 -0
  94. package/lib/types/vfs.d.ts +92 -0
  95. package/lib/types/vfs.js +217 -0
  96. package/locale/en.json +6 -0
  97. package/locale/zh.json +6 -0
  98. package/package.json +132 -3
  99. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The 3D look editor: a `parts3d` recipe as sliders and color wells,
3
+ * generated from the recipe itself — proportions, every part's numbers
4
+ * (built-in keys and a project part's own `extra` numbers alike), palette,
5
+ * motions and scale. Nothing here knows a theme: a project's own bodies and
6
+ * parts get the same controls as the built-ins.
7
+ */
8
+ import type { ReactNode } from 'react';
9
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
10
+ import type { Parts3DLook } from '../types.ts';
11
+ /** Props of {@link Recipe3DEditor}. */
12
+ export interface Recipe3DEditorProps {
13
+ readonly t: TranslateNS<'game'>;
14
+ readonly look: Parts3DLook;
15
+ readonly onChange: (next: Parts3DLook) => void;
16
+ }
17
+ /** The recipe editor. */
18
+ export declare function Recipe3DEditor({ t, look, onChange }: Recipe3DEditorProps): ReactNode;
19
+ //# sourceMappingURL=Recipe3DEditor.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { zh } from "./locales.js";
3
+ import css from './GameTab.module.css';
4
+ /** Integer counts and their ranges (the validator's, mirrored). */
5
+ const COUNT_RANGE = { legs: [0, 8], heads: [1, 9], segments: [3, 40], count: [1, 12] };
6
+ /** Part numbers and their ranges (the validator's, mirrored). */
7
+ const PART_RANGE = {
8
+ size: [0.05, 3, 0.01], length: [0.05, 3, 0.01], fan: [0, 360, 5], thickness: [0.01, 0.5, 0.005],
9
+ };
10
+ /** The controls each built-in part always gets, with the value dsh3d uses when the recipe leaves it out. */
11
+ const PART_DEFAULTS = {
12
+ ear: { count: 2, size: 0.15 }, horn: { count: 2, length: 0.25 }, tail: { count: 1, length: 0.9, fan: 0, thickness: 0.09 },
13
+ wing: { count: 2, size: 1 }, fin: { count: 2, size: 1 }, eye: { count: 2 },
14
+ };
15
+ /** A label for a recipe key: the dictionary's when it has one, else the key itself. */
16
+ function labelOf(t, key) {
17
+ const k = `recipe.key.${key}`;
18
+ return Object.hasOwn(zh, k) ? t(k) : key;
19
+ }
20
+ function Slider({ label, value, min, max, step, onChange }) {
21
+ return _jsxs("label", { className: css.param, children: [_jsx("span", { className: css.paramLabel, children: label }), _jsx("input", { type: "range", className: css.range, min: min, max: max, step: step, value: value, onChange: e => { onChange(Number(e.target.value)); } }), _jsx("span", { className: css.paramValue, children: step >= 1 ? String(Math.round(value)) : value.toFixed(step < 0.01 ? 3 : 2) })] });
22
+ }
23
+ /** The recipe editor. */
24
+ export function Recipe3DEditor({ t, look, onChange }) {
25
+ const setProportion = (key, v) => { onChange({ ...look, proportions: { ...look.proportions, [key]: v } }); };
26
+ const setPart = (index, patch) => {
27
+ onChange({ ...look, parts: look.parts.map((p, i) => (i === index ? { ...p, ...patch } : p)) });
28
+ };
29
+ const setMotion = (name, patch) => {
30
+ onChange({ ...look, motion: { ...look.motion, [name]: { ...look.motion[name], ...patch } } });
31
+ };
32
+ const proportions = Object.entries(look.proportions);
33
+ const motions = Object.entries(look.motion);
34
+ return _jsxs("div", { className: css.recipe, children: [_jsx("p", { className: css.metaLine, children: t('recipe.base', { base: labelOf(t, look.base) }) }), proportions.length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.proportions') }), proportions.map(([key, value]) => {
35
+ const count = COUNT_RANGE[key];
36
+ return _jsx(Slider, { label: labelOf(t, key), value: value, min: count?.[0] ?? 0.05, max: count?.[1] ?? 3, step: count === undefined ? 0.01 : 1, onChange: v => { setProportion(key, v); } }, key);
37
+ })] }), look.parts.length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.parts') }), look.parts.map((part, index) => _jsxs("div", { className: css.recipePart, children: [_jsx("strong", { className: css.recipePartName, children: labelOf(t, part.type) }), (part.count !== undefined || PART_DEFAULTS[part.type]?.count !== undefined) && _jsx(Slider, { label: labelOf(t, 'count'), value: part.count ?? PART_DEFAULTS[part.type].count, min: 1, max: 12, step: 1, onChange: v => { setPart(index, { count: v }); } }), ['size', 'length', 'fan', 'thickness'].filter(k => part[k] !== undefined || PART_DEFAULTS[part.type]?.[k] !== undefined).map(k => {
38
+ const [min, max, step] = PART_RANGE[k];
39
+ return _jsx(Slider, { label: labelOf(t, k), value: part[k] ?? PART_DEFAULTS[part.type][k], min: min, max: max, step: step, onChange: v => { setPart(index, { [k]: v }); } }, k);
40
+ }), Object.entries(part.extra ?? {}).filter((e) => typeof e[1] === 'number').map(([k, v]) => {
41
+ const span = Math.max(1, Math.abs(v) * 2);
42
+ return _jsx(Slider, { label: labelOf(t, k), value: v, min: v < 0 ? -span : 0, max: span, step: span > 20 ? 1 : 0.01, onChange: next => { setPart(index, { extra: { ...part.extra, [k]: next } }); } }, k);
43
+ })] }, index))] }), Object.keys(look.palette).length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.palette') }), _jsx("div", { className: css.recipeColors, children: Object.entries(look.palette).map(([key, color]) => _jsxs("label", { className: css.recipeColor, children: [_jsx("input", { type: "color", value: color, onChange: e => { onChange({ ...look, palette: { ...look.palette, [key]: e.target.value } }); } }), _jsx("span", { children: labelOf(t, key) })] }, key)) })] }), motions.length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.motion') }), motions.map(([name, m]) => _jsxs("div", { className: css.recipePart, children: [_jsx("strong", { className: css.recipePartName, children: labelOf(t, name) }), _jsx(Slider, { label: labelOf(t, 'amplitude'), value: m.amplitude ?? 0, min: 0, max: name === 'breath' || name === 'bob' ? 0.5 : 90, step: name === 'breath' || name === 'bob' ? 0.005 : 1, onChange: v => { setMotion(name, { amplitude: v }); } }), _jsx(Slider, { label: labelOf(t, 'period'), value: m.period ?? 1.5, min: 0.1, max: 10, step: 0.1, onChange: v => { setMotion(name, { period: v }); } })] }, name))] }), _jsx("section", { className: css.recipeGroup, children: _jsx(Slider, { label: labelOf(t, 'scale'), value: look.scale ?? 1, min: 0.2, max: 5, step: 0.05, onChange: v => { onChange({ ...look, scale: v }); } }) })] });
44
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The art-style gallery (decision D10): the same little scene drawn in each
3
+ * built-in style, so the choice is made by looking. Before a project exists
4
+ * a pick puts 「美术风格选 X」 into the composer; inside a project it asks
5
+ * for a restyle — everything is redrawn and the result always waits as a
6
+ * candidate.
7
+ */
8
+ import { type ReactNode } from 'react';
9
+ import type { ViewProps } from './contract.ts';
10
+ /** Props of the gallery. */
11
+ export interface StyleGalleryProps {
12
+ readonly t: ViewProps['t'];
13
+ readonly current?: string | undefined;
14
+ readonly onPick: (styleId: string, label: string) => void;
15
+ }
16
+ /** The gallery grid. */
17
+ export declare function StyleGallery({ t, current, onPick }: StyleGalleryProps): ReactNode;
18
+ //# sourceMappingURL=StyleGallery.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The art-style gallery (decision D10): the same little scene drawn in each
4
+ * built-in style, so the choice is made by looking. Before a project exists
5
+ * a pick puts 「美术风格选 X」 into the composer; inside a project it asks
6
+ * for a restyle — everything is redrawn and the result always waits as a
7
+ * candidate.
8
+ */
9
+ import { useEffect, useRef } from 'react';
10
+ import { ART_STYLES } from "../types.js";
11
+ import { drawStyleSample } from "./style-samples.js";
12
+ import css from './GameTab.module.css';
13
+ /** One sample canvas. */
14
+ function Sample({ styleId }) {
15
+ const ref = useRef(null);
16
+ useEffect(() => {
17
+ const canvas = ref.current;
18
+ const g = canvas?.getContext('2d');
19
+ if (canvas && g)
20
+ drawStyleSample(g, styleId, canvas.width, canvas.height);
21
+ }, [styleId]);
22
+ return _jsx("canvas", { ref: ref, className: css.sample, width: 200, height: 112, "aria-hidden": "true" });
23
+ }
24
+ /** The gallery grid. */
25
+ export function StyleGallery({ t, current, onPick }) {
26
+ return _jsx("div", { className: css.gallery, children: ART_STYLES.map(style => _jsxs("button", { type: "button", className: css.styleCard, "aria-pressed": current === style.id, onClick: () => { onPick(style.id, style.label); }, children: [_jsx(Sample, { styleId: style.id }), _jsxs("span", { className: css.styleName, children: [style.label, current === style.id && _jsx("span", { className: css.badge, children: t('style.current') })] }), _jsx("span", { className: css.metaLine, children: style.look }), _jsx("span", { className: css.paletteStrip, "aria-hidden": "true", children: style.palette.slice(0, 8).map(c => _jsx("span", { style: { background: c } }, c)) })] }, style.id)) });
27
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The turn card: beneath each finished agent turn (the `conversation.chat.turnTail`
3
+ * seat, outside the fold — the canvas-novel precedent), what the turn changed
4
+ * in the game: the version that went live or waits as a candidate, the
5
+ * check, elements, design chapters, parameters, code and tests — each with a
6
+ * jump to the place in the game tab where the change can be seen.
7
+ */
8
+ import { type ReactNode } from 'react';
9
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
10
+ import type { GameTabInjected } from './contract.ts';
11
+ /** The key this package's turn data lives under. */
12
+ export declare const GAME_TURN_KEY = "dsh-game-turn";
13
+ interface EventLike {
14
+ readonly type: string;
15
+ readonly data?: unknown;
16
+ }
17
+ interface TurnState {
18
+ readonly turn: number;
19
+ readonly ended: boolean;
20
+ }
21
+ interface LocationData {
22
+ readonly kind: 'turn';
23
+ readonly turn: number;
24
+ readonly key: string;
25
+ readonly value: TurnState;
26
+ }
27
+ /** Tracks each turn's number and whether it ended; the report itself comes from the host. */
28
+ export declare const gameTurnDefinition: {
29
+ kind: string;
30
+ match(event: EventLike): {
31
+ id: string;
32
+ role: "start" | "update";
33
+ } | null;
34
+ start(_context: unknown, match: {
35
+ event: EventLike;
36
+ }): TurnState;
37
+ update(context: {
38
+ readonly state: TurnState;
39
+ }, match: {
40
+ event: EventLike;
41
+ }): TurnState;
42
+ buildLocationData(context: {
43
+ readonly state: TurnState | undefined;
44
+ }, scope: string, previous: LocationData | null | undefined): LocationData | null;
45
+ };
46
+ /** Props of the card (ui-chat's turn-tail owner props, structurally, plus our face). */
47
+ export type GameTurnCardProps = GameTabInjected & {
48
+ readonly sessionId: string;
49
+ readonly turn: {
50
+ readonly data: {
51
+ get: (key: string) => unknown;
52
+ };
53
+ readonly end?: {
54
+ readonly seq: number;
55
+ } | null;
56
+ };
57
+ readonly t: TranslateNS<'game'>;
58
+ };
59
+ /** The card. Renders nothing until the turn ended and only when it changed something. */
60
+ export declare function GameTurnCard(props: GameTurnCardProps): ReactNode;
61
+ export {};
62
+ //# sourceMappingURL=TurnCard.d.ts.map
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The turn card: beneath each finished agent turn (the `conversation.chat.turnTail`
4
+ * seat, outside the fold — the canvas-novel precedent), what the turn changed
5
+ * in the game: the version that went live or waits as a candidate, the
6
+ * check, elements, design chapters, parameters, code and tests — each with a
7
+ * jump to the place in the game tab where the change can be seen.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import { normalizeLook3d } from "../look3d.js";
11
+ import { Preview3D } from "./Preview3D.js";
12
+ import css from './TurnCard.module.css';
13
+ /** The key this package's turn data lives under. */
14
+ export const GAME_TURN_KEY = 'dsh-game-turn';
15
+ function turnOf(event) {
16
+ const turn = event.data?.turn;
17
+ return typeof turn === 'number' ? turn : null;
18
+ }
19
+ /** Tracks each turn's number and whether it ended; the report itself comes from the host. */
20
+ export const gameTurnDefinition = {
21
+ kind: GAME_TURN_KEY,
22
+ match(event) {
23
+ const turn = turnOf(event);
24
+ if (turn === null)
25
+ return null;
26
+ if (event.type === 'turn/start')
27
+ return { id: String(turn), role: 'start' };
28
+ if (event.type === 'turn/end')
29
+ return { id: String(turn), role: 'update' };
30
+ return null;
31
+ },
32
+ start(_context, match) {
33
+ return { turn: turnOf(match.event) ?? -1, ended: false };
34
+ },
35
+ update(context, match) {
36
+ return match.event.type === 'turn/end' ? { ...context.state, ended: true } : context.state;
37
+ },
38
+ buildLocationData(context, scope, previous) {
39
+ if (scope !== 'turn' || context.state === undefined)
40
+ return null;
41
+ if (previous?.kind === 'turn' && previous.turn === context.state.turn && previous.value.ended === context.state.ended)
42
+ return previous;
43
+ return { kind: 'turn', turn: context.state.turn, key: GAME_TURN_KEY, value: context.state };
44
+ },
45
+ };
46
+ /** One line of the card. */
47
+ function Row({ icon, tone, children, go, t, onGo }) {
48
+ return _jsxs("li", { className: css.row, "data-tone": tone, children: [_jsx("span", { className: css.icon, "aria-hidden": "true", children: icon }), _jsx("span", { className: css.text, children: children }), go !== undefined && _jsx("button", { type: "button", className: css.go, onClick: () => { onGo(go); }, children: t('turn.see') })] });
49
+ }
50
+ /** A 3D look's change as two stills side by side (rendered lazily in the sandbox, then cached as images). */
51
+ function BeforeAfter({ t, api, projectId, version, looks }) {
52
+ const parse = (json) => {
53
+ if (json === null)
54
+ return null;
55
+ try {
56
+ return normalizeLook3d(JSON.parse(json));
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ };
62
+ const before = parse(looks.before), after = parse(looks.after);
63
+ if (after === null)
64
+ return null;
65
+ return _jsxs("span", { className: css.beforeAfter, children: [before !== null && _jsxs("figure", { children: [_jsx(Preview3D, { api: api, projectId: projectId, version: version, look: before, mode: "still", className: css.still, emptyLabel: "" }), _jsx("figcaption", { children: t('turn.lookBefore') })] }), before !== null && _jsx("span", { className: css.arrow, "aria-hidden": "true", children: "\u2192" }), _jsxs("figure", { children: [_jsx(Preview3D, { api: api, projectId: projectId, version: version, look: after, mode: "still", className: css.still, emptyLabel: "" }), _jsx("figcaption", { children: t('turn.lookAfter') })] })] });
66
+ }
67
+ /** The card. Renders nothing until the turn ended and only when it changed something. */
68
+ export function GameTurnCard(props) {
69
+ const { t, api, sessionId, openAt, subscribeSessionEvents } = props;
70
+ let state;
71
+ try {
72
+ state = props.turn.data.get(GAME_TURN_KEY) ?? undefined;
73
+ }
74
+ catch {
75
+ state = undefined;
76
+ }
77
+ const turn = state?.turn;
78
+ const ended = state?.ended === true || props.turn.end != null;
79
+ const [report, setReport] = useState(null);
80
+ const [rev, setRev] = useState(0);
81
+ useEffect(() => subscribeSessionEvents(id => { if (id === sessionId)
82
+ setRev(n => n + 1); }), [subscribeSessionEvents, sessionId]);
83
+ useEffect(() => {
84
+ if (turn === undefined || !ended)
85
+ return;
86
+ let live = true;
87
+ // The host writes the report right after turn/end; a second read covers the race.
88
+ const read = () => api.turnReport(sessionId, turn).then(r => { if (live && r.ok)
89
+ setReport(r.value); }).catch(() => { });
90
+ void read();
91
+ const again = setTimeout(() => { void read(); }, 1500);
92
+ return () => { live = false; clearTimeout(again); };
93
+ }, [api, sessionId, turn, ended, rev]);
94
+ if (report === null)
95
+ return null;
96
+ const looks = report.elements.filter(e => e.what === 'look' || e.what === 'new');
97
+ const fieldOnly = report.elements.filter(e => e.what === 'fields');
98
+ const removed = report.elements.filter(e => e.what === 'removed');
99
+ return _jsxs("section", { className: css.card, "aria-label": t('turn.title', { title: report.title }), children: [_jsxs("header", { className: css.head, children: [_jsx("span", { className: css.badge, "aria-hidden": "true", children: "\u25C6" }), _jsx("strong", { children: t('turn.title', { title: report.title }) }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.open, onClick: () => { openAt({ view: 'play' }); }, children: t('turn.openPanel') })] }), _jsxs("ul", { className: css.list, children: [report.version !== null && _jsxs(Row, { t: t, onGo: openAt, icon: "\u25B6", tone: report.version.kind === 'deployed' ? 'ok' : 'warn', go: { view: 'play' }, children: [report.version.kind === 'deployed' ? t('turn.deployed', { version: report.version.version }) : t('turn.candidate', { version: report.version.version }), report.check !== null && report.check.ok && _jsxs("span", { className: css.meta, children: [t('turn.checkOk', { passed: report.check.passed }), report.check.fixes > 0 ? ` · ${t('turn.fixed', { count: report.check.fixes })}` : ''] })] }), report.check !== null && !report.check.ok && _jsx(Row, { t: t, onGo: openAt, icon: "!", tone: report.check.gaveUp ? 'warn' : 'error', go: { view: 'versions' }, children: report.check.gaveUp ? t('turn.gaveUp', { count: report.check.failures }) : t('turn.checkFailed', { count: report.check.failures }) }), looks.map(e => _jsxs(Row, { t: t, onGo: openAt, icon: "\u25C7", go: { view: 'elements', elementId: e.id }, children: [e.what === 'new' ? t('turn.elementNew', { name: e.name }) : t('turn.elementLook', { name: e.name }), e.looks !== undefined && _jsx(BeforeAfter, { t: t, api: api, projectId: report.projectId, version: report.version?.version ?? 0, looks: e.looks })] }, `l${e.id}`)), fieldOnly.length > 0 && fieldOnly.map(e => _jsx(Row, { t: t, onGo: openAt, icon: "\u25C7", go: { view: 'elements', elementId: e.id }, children: t('turn.elementFields', { name: e.name, fields: (e.fields ?? []).slice(0, 4).join('、') }) }, `f${e.id}`)), removed.length > 0 && _jsx(Row, { t: t, onGo: openAt, icon: "\u2212", go: { view: 'elements' }, children: t('turn.elementRemoved', { names: removed.map(e => e.name).join('、') }) }), report.chapters.map(c => _jsx(Row, { t: t, onGo: openAt, icon: "\u00B6", go: { view: 'design', chapterId: c.id }, children: t('turn.chapter', { title: c.title }) }, `c${c.id}`)), report.params.length > 0 && _jsx(Row, { t: t, onGo: openAt, icon: "\u2261", go: { view: 'play' }, children: t('turn.params', { list: report.params.slice(0, 4).map(p => (p.from === undefined ? `${p.label} ${p.to}` : `${p.label} ${p.from}→${p.to}`)).join('、') }) }), report.code.length > 0 && _jsxs(Row, { t: t, onGo: openAt, icon: "{}", go: { view: 'versions' }, children: [t('turn.code', { count: report.code.length }), _jsxs("span", { className: css.meta, children: [report.code.slice(0, 3).join('、'), report.code.length > 3 ? '…' : ''] })] }), report.tests.length > 0 && _jsx(Row, { t: t, onGo: openAt, icon: "\u2713", go: { view: 'versions' }, children: t('turn.tests', { count: report.tests.length }) })] })] });
100
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 版本: the last check (verdict, findings, the screenshot the headless run
3
+ * took), the version timeline (current / candidate / rejected / past, with
4
+ * 切回 and 与当前对比), the playtest log, and the single-file export.
5
+ */
6
+ import { type ReactNode } from 'react';
7
+ import type { ProjectView } from '../types.ts';
8
+ import { type ViewProps } from './contract.ts';
9
+ /** Props of the versions view. */
10
+ export interface VersionsViewProps extends ViewProps {
11
+ readonly project: ProjectView;
12
+ }
13
+ /** `MM-DD HH:mm` in local time. */
14
+ export declare function shortTime(iso: string): string;
15
+ /** The versions view. */
16
+ export declare function VersionsView({ t, sessionId, api, project, refresh, toast }: VersionsViewProps): ReactNode;
17
+ //# sourceMappingURL=VersionsView.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * 版本: the last check (verdict, findings, the screenshot the headless run
4
+ * took), the version timeline (current / candidate / rejected / past, with
5
+ * 切回 and 与当前对比), the playtest log, and the single-file export.
6
+ */
7
+ import { useEffect, useState } from 'react';
8
+ import { exportHtml } from "../vfs.js";
9
+ import { failureOf } from "./contract.js";
10
+ import css from './GameTab.module.css';
11
+ /** `MM-DD HH:mm` in local time. */
12
+ export function shortTime(iso) {
13
+ const d = new Date(iso);
14
+ if (Number.isNaN(d.getTime()))
15
+ return iso;
16
+ const pad = (n) => String(n).padStart(2, '0');
17
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
18
+ }
19
+ /** Download a text file from the panel (a user gesture in the host page). */
20
+ function download(name, text) {
21
+ const url = URL.createObjectURL(new Blob([text], { type: 'text/html' }));
22
+ const a = document.createElement('a');
23
+ a.href = url;
24
+ a.download = name;
25
+ a.click();
26
+ setTimeout(() => { URL.revokeObjectURL(url); }, 2000);
27
+ }
28
+ /** The versions view. */
29
+ export function VersionsView({ t, sessionId, api, project, refresh, toast }) {
30
+ const check = project.lastCheck;
31
+ const [shot, setShot] = useState(null);
32
+ const [diffFor, setDiffFor] = useState(null);
33
+ const [diff, setDiff] = useState(null);
34
+ const current = project.current?.version;
35
+ useEffect(() => {
36
+ let live = true;
37
+ void api.checkShot(project.id).then(r => { if (live)
38
+ setShot(r.ok && r.value.ok ? r.value.webp : null); });
39
+ return () => { live = false; };
40
+ }, [api, project.id, check?.at]);
41
+ useEffect(() => {
42
+ if (diffFor === null || current === undefined) {
43
+ setDiff(null);
44
+ return;
45
+ }
46
+ let live = true;
47
+ void api.diff(project.id, diffFor, current).then(r => { if (live)
48
+ setDiff(r.ok && r.value.ok ? r.value.diff : null); });
49
+ return () => { live = false; };
50
+ }, [api, project.id, diffFor, current]);
51
+ const exportVersion = (version) => {
52
+ void api.versionBundle(project.id, version).then(r => {
53
+ if (!(r.ok && r.value.ok)) {
54
+ toast(t('versions.exportFailed'));
55
+ return;
56
+ }
57
+ download(`${project.title}-v${version}.html`, exportHtml(project.title, r.value.bundle.files));
58
+ toast(t('toast.exported', { version }));
59
+ });
60
+ };
61
+ return _jsxs("div", { className: css.scroll, children: [_jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.lastCheck') }), check === null
62
+ ? _jsx("p", { className: css.lead, children: t('checks.none') })
63
+ : _jsxs("div", { className: css.checkCard, "data-tone": check.ok ? 'ok' : check.gaveUp ? 'warn' : 'error', children: [_jsxs("div", { className: css.checkHead, children: [_jsx("strong", { children: check.ok ? t('checks.ok') : t('checks.fail') }), _jsxs("span", { className: css.metaLine, children: [shortTime(check.at), check.attempt > 0 ? ` · ${t('checks.attempt', { attempt: check.attempt })}` : '', check.passed > 0 ? ` · ${t('checks.passed', { count: check.passed })}` : ''] })] }), _jsxs("ol", { className: css.layers, children: [_jsx("li", { "data-ok": check.failures.some(f => f.where === 'data' || f.where.startsWith('elements/')) ? 'false' : 'true', children: t('checks.layerData') }), _jsx("li", { "data-ok": check.skipped !== undefined ? 'skip' : check.failures.some(f => f.where === 'smoke') ? 'false' : 'true', children: t('checks.layerSmoke') }), _jsx("li", { "data-ok": check.skipped !== undefined ? 'skip' : check.failures.some(f => f.where.startsWith('tests/')) ? 'false' : 'true', children: t('checks.layerTests') })] }), check.gaveUp === true && _jsx("p", { className: css.warnText, children: t('checks.gaveUp') }), check.skipped !== undefined && _jsx("p", { className: css.metaLine, children: check.skipped }), check.failures.length > 0 && _jsx("ul", { className: css.findings, children: check.failures.map((f, i) => _jsxs("li", { children: [_jsx("code", { children: f.where }), _jsx("span", { children: f.message })] }, i)) }), shot !== null && _jsxs("figure", { className: css.shot, children: [_jsx("img", { src: `data:image/webp;base64,${shot}`, alt: t('versions.shotAlt') }), _jsx("figcaption", { className: css.metaLine, children: t('versions.shotCaption') })] })] })] }), _jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.title') }), project.versions.length === 0 && _jsx("p", { className: css.lead, children: t('versions.none') }), _jsx("ol", { className: css.timeline, children: project.versions.map((v) => _jsxs("li", { className: css.tlItem, "data-status": v.status, children: [_jsx("span", { className: css.tlDot, "aria-hidden": "true" }), _jsxs("div", { className: css.tlBody, children: [_jsxs("div", { className: css.tlHead, children: [_jsxs("strong", { children: ["v", v.version] }), _jsx("span", { className: css.tlTag, "data-status": v.status, children: t(`versions.status.${v.status}`) }), v.by !== undefined && _jsx("span", { className: css.tlBy, "data-by": v.by, children: t(`versions.by.${v.by}`) }), _jsx("span", { className: css.metaLine, children: shortTime(v.at) })] }), v.note !== undefined && _jsx("span", { className: css.metaLine, children: v.note }), _jsxs("div", { className: css.tlActions, children: [v.status !== 'current' && v.status !== 'candidate' && _jsx("button", { type: "button", className: css.link, onClick: () => {
64
+ void api.rollback(sessionId, project.id, v.version).then(r => { if (failureOf(r) === undefined)
65
+ toast(t('toast.rolledBack', { version: v.version })); refresh(); });
66
+ }, children: t('versions.rollback') }), current !== undefined && v.version !== current && _jsx("button", { type: "button", className: css.link, "aria-pressed": diffFor === v.version, onClick: () => { setDiffFor(d => (d === v.version ? null : v.version)); }, children: t('versions.compare', { version: current }) }), _jsx("button", { type: "button", className: css.link, onClick: () => { exportVersion(v.version); }, children: t('versions.export') })] }), diffFor === v.version && diff !== null && _jsxs("div", { className: css.diff, children: [diff.added.length + diff.removed.length + diff.changed.length === 0 && _jsx("span", { className: css.metaLine, children: t('versions.same') }), diff.changed.map(f => _jsxs("span", { "data-kind": "changed", children: ["~ ", f] }, f)), diff.added.map(f => _jsxs("span", { "data-kind": "added", children: ["+ ", f] }, f)), diff.removed.map(f => _jsxs("span", { "data-kind": "removed", children: ["\u2212 ", f] }, f))] })] })] }, v.version)) })] }), _jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.share') }), _jsx("p", { className: css.metaLine, children: t('versions.shareNote') }), _jsx("div", { className: css.rowEnd, children: _jsx("button", { type: "button", className: css.primary, disabled: current === undefined, onClick: () => { if (current !== undefined)
67
+ exportVersion(current); }, children: t('versions.exportCurrent') }) }), _jsx("p", { className: css.metaLine, children: t('versions.later') })] }), _jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.log') }), project.log.length === 0 && _jsx("p", { className: css.metaLine, children: t('versions.logEmpty') }), _jsx("ul", { className: css.logList, children: project.log.slice(0, 30).map((e, i) => _jsxs("li", { children: [_jsx("span", { className: css.logTime, children: shortTime(e.at) }), _jsx("span", { children: e.text })] }, i)) })] })] });
68
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The game's rows in the conversation (proposal option B): the messages the
3
+ * plugin puts into a turn — check findings steered back to the agent, and
4
+ * the panel changes attached to a send — are `dsh-game`-sourced user
5
+ * messages, which the official chat view projects as hidden `context` nodes.
6
+ * This Definition projects the same events into our own node kind and a
7
+ * keyed `conversation.chat.node` renderer draws them as one collapsible row:
8
+ * red 「检查未通过 1/2」 with the findings, grey 「已附上」 with the items.
9
+ * The official node stays hidden, so nothing renders twice (the
10
+ * message-tools restored-row precedent).
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
14
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
15
+ /** The chat node kind (also the renderer's key). */
16
+ export declare const GAME_CHAT_KIND = "dsh-game-message";
17
+ /** What the row needs from the persisted message. */
18
+ export interface GameChatData {
19
+ readonly seq: number;
20
+ readonly time: number;
21
+ readonly op: 'check-failed' | 'attached' | 'other';
22
+ readonly attempt: number;
23
+ readonly cap: number;
24
+ readonly failures: readonly {
25
+ readonly where: string;
26
+ readonly message: string;
27
+ }[];
28
+ readonly items: readonly string[];
29
+ readonly text: string;
30
+ }
31
+ declare module '@deepseek-ai/dsh-client-ui-chat/client' {
32
+ interface ChatNodeDataMap {
33
+ 'dsh-game-message': GameChatData;
34
+ }
35
+ }
36
+ /** The projection: every appended `dsh-game` user message becomes one row. */
37
+ export declare const gameMessageDefinition: ConversationNodeDefinition<GameChatData>;
38
+ /** Props of the row renderer. */
39
+ export type GameChatRowProps = PropsRuntime<'conversation.chat.node', 'dsh-game-message'> & PropsLocale<'game'>;
40
+ /** The collapsible row. */
41
+ export declare function GameChatRow({ node, t }: GameChatRowProps): ReactNode;
42
+ //# sourceMappingURL=chat-node.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The game's rows in the conversation (proposal option B): the messages the
4
+ * plugin puts into a turn — check findings steered back to the agent, and
5
+ * the panel changes attached to a send — are `dsh-game`-sourced user
6
+ * messages, which the official chat view projects as hidden `context` nodes.
7
+ * This Definition projects the same events into our own node kind and a
8
+ * keyed `conversation.chat.node` renderer draws them as one collapsible row:
9
+ * red 「检查未通过 1/2」 with the findings, grey 「已附上」 with the items.
10
+ * The official node stays hidden, so nothing renders twice (the
11
+ * message-tools restored-row precedent).
12
+ */
13
+ import { useState } from 'react';
14
+ import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
15
+ import css from './ChatRow.module.css';
16
+ /** The chat node kind (also the renderer's key). */
17
+ export const GAME_CHAT_KIND = 'dsh-game-message';
18
+ /** Whether a message source is ours. */
19
+ function isGameSource(source) {
20
+ return source?.kind === 'dsh-game';
21
+ }
22
+ /** The source's structured part, defensively. */
23
+ function dataOf(seq, time, source, content) {
24
+ const text = content.map(block => block.type === 'text' ? block.text : '').join('\n');
25
+ const op = source.op === 'check-failed' || source.op === 'attached' ? source.op : 'other';
26
+ const failures = Array.isArray(source.failures)
27
+ ? source.failures.filter((f) => typeof f?.where === 'string' && typeof f.message === 'string')
28
+ : [];
29
+ const items = Array.isArray(source.items) ? source.items.filter((i) => typeof i === 'string') : [];
30
+ return {
31
+ seq, time, op, text, failures, items,
32
+ attempt: typeof source.attempt === 'number' ? source.attempt : 0,
33
+ cap: typeof source.cap === 'number' ? source.cap : 2,
34
+ };
35
+ }
36
+ /** The projection: every appended `dsh-game` user message becomes one row. */
37
+ export const gameMessageDefinition = {
38
+ kind: GAME_CHAT_KIND,
39
+ target: 'chat',
40
+ match: (event) => {
41
+ if (event.type !== 'user/message' || !isAppendSurfaceEvent(event) || !isGameSource(event.data.source))
42
+ return null;
43
+ return { id: String(event.seq), role: 'start' };
44
+ },
45
+ start: (_context, match) => {
46
+ const event = match.event;
47
+ if (event.type !== 'user/message' || !isGameSource(event.data.source))
48
+ throw new Error('dsh-game row requires a dsh-game message');
49
+ return dataOf(event.seq, event.time, event.data.source, event.data.content);
50
+ },
51
+ update: context => context.state,
52
+ buildViewNode: (context) => context.state === undefined
53
+ ? null
54
+ : {
55
+ key: context.key,
56
+ kind: GAME_CHAT_KIND,
57
+ id: context.id,
58
+ target: 'chat',
59
+ anchorSeq: context.state.seq,
60
+ location: context.start?.location ?? { kind: 'unresolved' },
61
+ visibility: 'visible',
62
+ data: context.state,
63
+ },
64
+ };
65
+ /** The collapsible row. */
66
+ export function GameChatRow({ node, t }) {
67
+ const data = node.data;
68
+ const [open, setOpen] = useState(false);
69
+ if (data.op === 'attached') {
70
+ return _jsxs("div", { className: css.wrap, "data-tone": "muted", "data-align": "end", children: [_jsxs("button", { type: "button", className: css.head, "aria-expanded": open, onClick: () => { setOpen(o => !o); }, children: [_jsx("span", { className: css.dot, "aria-hidden": "true" }), _jsx("span", { className: css.title, children: t('chat.attached', { count: data.items.length }) }), _jsxs("span", { className: css.summary, children: [data.items.slice(0, 2).join(' · '), data.items.length > 2 ? ' …' : ''] }), _jsx("span", { className: css.chevron, "aria-hidden": "true", children: open ? '▾' : '▸' })] }), open && _jsx("ul", { className: css.list, children: data.items.map((item, i) => _jsx("li", { children: item }, i)) })] });
71
+ }
72
+ if (data.op === 'check-failed') {
73
+ const last = data.attempt >= data.cap;
74
+ return _jsxs("div", { className: css.wrap, "data-tone": last ? 'warn' : 'error', children: [_jsxs("button", { type: "button", className: css.head, "aria-expanded": open, onClick: () => { setOpen(o => !o); }, children: [_jsx("span", { className: css.dot, "aria-hidden": "true" }), _jsx("span", { className: css.title, children: t('chat.checkFailed', { attempt: data.attempt, cap: data.cap }) }), _jsx("span", { className: css.summary, children: data.failures[0] === undefined ? '' : `${data.failures[0].where}:${data.failures[0].message}` }), _jsx("span", { className: css.chevron, "aria-hidden": "true", children: open ? '▾' : '▸' })] }), open && _jsxs("div", { className: css.body, children: [_jsx("ul", { className: css.list, children: data.failures.map((f, i) => _jsxs("li", { children: [_jsx("code", { children: f.where }), _jsx("span", { children: f.message })] }, i)) }), _jsx("p", { className: css.note, children: last ? t('chat.lastAttempt') : t('chat.fixing') })] })] });
75
+ }
76
+ return _jsxs("div", { className: css.wrap, "data-tone": "muted", children: [_jsxs("button", { type: "button", className: css.head, "aria-expanded": open, onClick: () => { setOpen(o => !o); }, children: [_jsx("span", { className: css.dot, "aria-hidden": "true" }), _jsx("span", { className: css.title, children: t('chat.other') }), _jsx("span", { className: css.chevron, "aria-hidden": "true", children: open ? '▾' : '▸' })] }), open && _jsx("pre", { className: css.pre, children: data.text })] });
77
+ }
@@ -0,0 +1,125 @@
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 { BundleFile, CommitResult, DeleteProjectResult, WorkingChanges, GameResult, PlayBundle, ProjectCard, SessionGameState, TurnReport, VersionDiff } from '../types.ts';
9
+ type R<T> = Promise<RemoteResult<T>>;
10
+ type Ok<T extends object = object> = GameResult<{
11
+ readonly ok: true;
12
+ } & T>;
13
+ /** Every call the panel makes, already bound to the Remote. */
14
+ export interface GameApi {
15
+ state: (sid: SessionId) => R<SessionGameState>;
16
+ projectCards: () => R<ProjectCard[]>;
17
+ turnReport: (sid: SessionId, turn: number) => R<TurnReport | null>;
18
+ bind: (sid: SessionId, projectId: string | null) => R<Ok>;
19
+ deleteProject: (sid: SessionId, projectId: string, mode: 'trash' | 'forget') => R<DeleteProjectResult>;
20
+ bundle: (projectId: string, which: 'current' | 'candidate') => R<Ok<{
21
+ readonly bundle: PlayBundle;
22
+ }>>;
23
+ versionBundle: (projectId: string, version: number) => R<Ok<{
24
+ readonly bundle: PlayBundle;
25
+ }>>;
26
+ workingFile: (projectId: string, path: string) => R<Ok<{
27
+ readonly file: BundleFile;
28
+ }>>;
29
+ confirm: (sid: SessionId, projectId: string) => R<Ok<{
30
+ readonly version: number;
31
+ }>>;
32
+ commit: (sid: SessionId, projectId: string) => R<GameResult<CommitResult>>;
33
+ changes: (projectId: string) => R<GameResult<WorkingChanges & {
34
+ readonly ok: true;
35
+ }>>;
36
+ reject: (sid: SessionId, projectId: string, reason: string) => R<Ok>;
37
+ rollback: (sid: SessionId, projectId: string, version: number) => R<Ok>;
38
+ diff: (projectId: string, from: number, to: number) => R<Ok<{
39
+ readonly diff: VersionDiff;
40
+ }>>;
41
+ checkShot: (projectId: string, version?: number) => R<Ok<{
42
+ readonly webp: string;
43
+ }>>;
44
+ setAutoConfirm: (sid: SessionId, projectId: string, on: boolean) => R<Ok>;
45
+ logPlay: (sid: SessionId, entry: {
46
+ projectId: string;
47
+ version: number;
48
+ seconds: number;
49
+ errors: string[];
50
+ }) => R<Ok>;
51
+ setParam: (sid: SessionId, projectId: string, key: string, value: number) => R<Ok>;
52
+ finalizeParams: (sid: SessionId, projectId: string) => R<Ok>;
53
+ resetParams: (sid: SessionId, projectId: string) => R<Ok>;
54
+ editElement: (sid: SessionId, projectId: string, elementId: string, patch: {
55
+ look?: string;
56
+ fields?: string;
57
+ }) => R<Ok<{
58
+ readonly elementJson: string;
59
+ }>>;
60
+ pickProposal: (sid: SessionId, projectId: string, elementId: string, at: string, index: number) => R<Ok>;
61
+ writeChapter: (sid: SessionId, projectId: string, chapterId: string, text: string) => R<Ok>;
62
+ requestRestyle: (sid: SessionId, projectId: string, styleId: string) => R<Ok>;
63
+ setOnePager: (sid: SessionId, projectId: string, patch: {
64
+ pitch?: string;
65
+ elementTypes?: string;
66
+ pillars?: string;
67
+ }) => R<Ok>;
68
+ dropPending: (sid: SessionId, projectId: string, key: string) => R<Ok>;
69
+ }
70
+ /** Where a jump into the game tab lands. */
71
+ export interface NavTarget {
72
+ readonly view: 'play' | 'elements' | 'design' | 'versions';
73
+ readonly elementId?: string;
74
+ readonly chapterId?: string;
75
+ }
76
+ /** What the tab body gets from the plugin. */
77
+ export interface GameTabInjected {
78
+ /** Open the game tab at a place (the turn card's 查看). */
79
+ openAt: (target: NavTarget) => void;
80
+ /** The tab body follows jumps; returns the unsubscribe. */
81
+ subscribeNav: (listener: (target: NavTarget) => void) => () => void;
82
+ /** A jump made before the tab body mounted, taken once. */
83
+ takeNav: () => NavTarget | null;
84
+ api: GameApi;
85
+ /** Put text into the session's composer draft (appended; never sent). False when there is no composer. */
86
+ insertDraft: (sessionId: SessionId, text: string) => boolean;
87
+ /**
88
+ * Put a reference into the session's composer as one chip (`label` shown,
89
+ * `text` what the model receives); falls back to appending `text` as plain
90
+ * draft text where the host has no chip seam. Never sends.
91
+ */
92
+ insertRef: (sessionId: SessionId, ref: {
93
+ readonly label: string;
94
+ readonly text: string;
95
+ }) => boolean;
96
+ /** Turn-boundary pushes, plus the panel's own mutations, per session id. */
97
+ subscribeSessionEvents: (listener: (sessionId: string) => void) => () => void;
98
+ /** Tell every game surface of this page that a session's state moved. */
99
+ touch: (sessionId: SessionId) => void;
100
+ }
101
+ /** Full props of the game tab body. */
102
+ export type GameTabProps = PropsRuntime<'sidebar.right.pane.tab'> & InjectFace<GameTabInjected> & PropsLocale<'game'>;
103
+ /** What every view inside the tab receives. */
104
+ export interface ViewProps {
105
+ readonly t: GameTabProps['t'];
106
+ readonly sessionId: GameTabProps['sessionId'];
107
+ readonly api: GameApi;
108
+ readonly insertDraft: (text: string) => boolean;
109
+ /** Insert a reference chip into the composer (see `GameTabInjected.insertRef`). */
110
+ readonly insertRef: (ref: {
111
+ readonly label: string;
112
+ readonly text: string;
113
+ }) => boolean;
114
+ /** Re-read the state after a mutation (and tell the other game surfaces). */
115
+ readonly refresh: () => void;
116
+ /** Show a short confirmation line at the panel's foot. */
117
+ readonly toast: (text: string) => void;
118
+ }
119
+ /** A Remote call's game-level failure code, or undefined on success. */
120
+ export declare function failureOf(result: RemoteResult<{
121
+ ok: boolean;
122
+ error?: string;
123
+ }>): string | undefined;
124
+ export {};
125
+ //# 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
+ }