@jini-ai/ui 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +20 -0
- package/dist/core/theme/default.d.ts +4 -0
- package/dist/core/theme/default.js +11 -0
- package/dist/core/theme/stylesheet.d.ts +8 -0
- package/dist/core/theme/stylesheet.js +14 -0
- package/dist/core/theme/types.d.ts +39 -0
- package/dist/core/theme/types.js +2 -0
- package/dist/core/theme/validation.d.ts +15 -0
- package/dist/core/theme/validation.js +55 -0
- package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
- package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
- package/dist/features/admin-widgets/components/Select/Select.js +4 -3
- package/dist/features/appearance/constants.js +1 -1
- package/dist/features/connectors/rules.d.ts +1 -6
- package/dist/features/connectors/types.d.ts +1 -6
- package/dist/features/connectors/types.js +1 -6
- package/dist/features/execution/http-adapter.d.ts +111 -0
- package/dist/features/execution/http-adapter.js +160 -0
- package/dist/features/execution/index.d.ts +4 -0
- package/dist/features/execution/index.js +2 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
- package/dist/features/html-editor/canvas-style.d.ts +1 -1
- package/dist/features/html-editor/canvas-style.js +2 -2
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
- package/dist/features/i18n/dictionary-translator.d.ts +5 -2
- package/dist/features/i18n/dictionary-translator.js +1 -1
- package/dist/features/i18n/index.d.ts +1 -1
- package/dist/features/i18n/index.js +1 -1
- package/dist/features/i18n/template-i18n.d.ts +11 -0
- package/dist/features/i18n/template-i18n.js +12 -0
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +1 -1
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +1 -1
- package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
- package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
- package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
- package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
- package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
- package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
- package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
- package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
- package/dist/features/lexical-rich-text-editor/types.js +3 -13
- package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
- package/dist/features/mcp-ui/early-message-buffer.js +1 -2
- package/dist/features/mcp-ui/escape.d.ts +4 -0
- package/dist/features/mcp-ui/escape.js +2 -9
- package/dist/features/mcp-ui/index.d.ts +1 -0
- package/dist/features/mcp-ui/index.js +1 -0
- package/dist/features/mcp-ui/protocol.d.ts +2 -5
- package/dist/features/mcp-ui/protocol.js +2 -5
- package/dist/features/mcp-ui/resource.d.ts +47 -7
- package/dist/features/mcp-ui/resource.js +5 -8
- package/dist/features/mcp-ui/secret-card.d.ts +115 -0
- package/dist/features/mcp-ui/secret-card.js +137 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
- package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
- package/dist/features/mcp-ui/surfaces/fields.js +3 -1
- package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
- package/dist/features/mcp-ui/surfaces/form.js +65 -3
- package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
- package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
- package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
- package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
- package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
- package/dist/features/panel-kit/helpers/permissions.js +2 -0
- package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
- package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
- package/dist/features/panel-kit/index.d.ts +7 -1
- package/dist/features/panel-kit/index.js +5 -1
- package/dist/features/resource-dashboard/types.d.ts +8 -41
- package/dist/features/resource-dashboard/types.js +3 -26
- package/dist/features/tab-strip/constants.d.ts +3 -4
- package/dist/features/tab-strip/constants.js +3 -4
- package/dist/features/tab-strip/dependencies.d.ts +1 -6
- package/dist/features/tab-strip/dependencies.js +1 -6
- package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
- package/dist/features/tab-strip/react/components/TabBar.js +12 -1
- package/dist/features/tab-strip/rules.d.ts +4 -17
- package/dist/features/tab-strip/rules.js +4 -17
- package/dist/features/tab-strip/types.d.ts +2 -13
- package/dist/icon-name.d.ts +1 -1
- package/dist/icon-name.js +1 -0
- package/dist/index.d.ts +42 -417
- package/dist/index.js +42 -430
- package/dist/react/components/CustomSelect.d.ts +6 -2
- package/dist/react/components/CustomSelect.js +20 -91
- package/dist/react/components/Icon.d.ts +6 -0
- package/dist/react/components/Icon.js +12 -0
- package/dist/react/components/select-popover.d.ts +106 -0
- package/dist/react/components/select-popover.js +210 -0
- package/dist/renderers/sandbox-scripts.d.ts +29 -0
- package/dist/renderers/sandbox-scripts.js +127 -0
- package/dist/renderers/sandboxed-document.js +3 -86
- package/dist/renderers/srcdoc/build.d.ts +10 -17
- package/dist/renderers/srcdoc/build.js +31 -234
- package/dist/styles/variables.css +2 -2
- package/dist/theme/apply.d.ts +1 -1
- package/dist/theme/apply.js +2 -2
- package/dist/theme/color-scheme.d.ts +1 -1
- package/dist/theme/default.d.ts +1 -3
- package/dist/theme/default.js +2 -10
- package/dist/theme/index.d.ts +3 -3
- package/dist/theme/index.js +2 -2
- package/dist/theme/stylesheet.d.ts +1 -7
- package/dist/theme/stylesheet.js +2 -13
- package/dist/theme/types.d.ts +1 -38
- package/dist/theme/types.js +2 -1
- package/dist/theme/validation.d.ts +1 -14
- package/dist/theme/validation.js +2 -54
- package/dist/utils/endpoint-policy.d.ts +21 -39
- package/dist/utils/endpoint-policy.js +19 -107
- package/dist/utils/file-system-errors.d.ts +1 -2
- package/dist/utils/file-system-errors.js +1 -2
- package/dist/utils/ime-composing.d.ts +0 -3
- package/dist/utils/ime-composing.js +0 -3
- package/dist/utils/platform.d.ts +1 -4
- package/dist/utils/platform.js +1 -4
- package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
- package/dist/utils/smooth-scroll-to-top.js +3 -13
- package/dist/utils/timezone.js +1 -5
- package/dist/utils/uuid.d.ts +0 -3
- package/dist/utils/uuid.js +0 -3
- package/dist/utils/visual-stability.d.ts +1 -3
- package/dist/utils/visual-stability.js +1 -3
- package/package.json +74 -15
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { createStandingDraftLocalBackup } from "../helpers/standing-draft-local-backup.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file Standing-draft autosave (2026-09-06 dispatch, owner's own words: "even if they haven't
|
|
4
|
+
* saved it, temp save it whenever they make a draft... it's just saving to... the database. It
|
|
5
|
+
* should be a standing draft anytime something is updated"). Persists to the server
|
|
6
|
+
* (`server/inbound/admin-http/routes/posts/autosave.ts`, `posts.autosave_json`) so a reload or an
|
|
7
|
+
* accidental exit never loses unsaved edits — browser storage alone was rejected.
|
|
8
|
+
*
|
|
9
|
+
* Feature-agnostic on purpose, matching `use-dirty-guard.hooks.ts`'s own precedent: nothing here
|
|
10
|
+
* imports `features/posts` or `features/pages`, so `PostEditorPort`/`PageEditorPort` each satisfy
|
|
11
|
+
* {@link StandingDraftAutosavePort} structurally (same three method shapes, declared independently
|
|
12
|
+
* per that DI-seam convention) with zero cross-feature import. This is the mechanism that lets one
|
|
13
|
+
* implementation serve both editors with no branch between them.
|
|
14
|
+
*
|
|
15
|
+
* Two things this hook does NOT do, deliberately:
|
|
16
|
+
* - Never auto-applies a recovered draft. {@link StandingDraftAutosaveController.recoverableDraft}
|
|
17
|
+
* is exposed for the caller to render an explicit "restore or discard" banner; silently splicing
|
|
18
|
+
* in a recovered draft is dangerous with two tabs or two operators open on the same entry.
|
|
19
|
+
* - Never compares `recoverableDraft.baseVersion` against the entry's current version itself — the
|
|
20
|
+
* caller (which already has the freshly-loaded record) decides whether to label a recovered draft
|
|
21
|
+
* "outdated" because a real save happened since it was captured.
|
|
22
|
+
*
|
|
23
|
+
* STALE BASIS (added 2026-09-06, fixing silent data loss). `putAutosave` answers `{ applied }`, and
|
|
24
|
+
* `applied: false` means the row's `version` moved under this tab — another tab or operator saved,
|
|
25
|
+
* so every further write against this basis will be refused too (the guard is
|
|
26
|
+
* `host features/post/repo.sqlite.ts`'s `writeAutosave`). That flag used to be thrown
|
|
27
|
+
* away: the hook kept firing refused ticks forever, nothing was ever parked, and a reload produced
|
|
28
|
+
* no recovery banner because there was nothing to recover — an hour of typing could vanish with the
|
|
29
|
+
* operator never told anything. Now a refusal:
|
|
30
|
+
* 1. records {@link StandingDraftStaleBasis} (exposed so the editor can say something true),
|
|
31
|
+
* 2. stops scheduling further writes until the caller supplies a draft on a DIFFERENT `baseVersion`
|
|
32
|
+
* — i.e. until the editor reloads the row; no manual reset call is needed,
|
|
33
|
+
* 3. mirrors the refused text into `lib/standing-draft-local-backup.ts`, the only place it can
|
|
34
|
+
* survive a tab close while the server refuses it, and offers it back through the existing
|
|
35
|
+
* `recoverableDraft` banner on the next mount when the server has nothing parked.
|
|
36
|
+
* The operator's in-memory text is never touched by any of this.
|
|
37
|
+
*
|
|
38
|
+
* The gate and the mirror have DIFFERENT lifetimes on purpose. The gate lifts the instant the
|
|
39
|
+
* caller edits on a fresh basis; the mirror survives until the server has actually accepted a write
|
|
40
|
+
* (or a real Save/discard superseded it), because between those two moments nothing has been
|
|
41
|
+
* persisted anywhere and the mirror is still the only copy of the refused text that outlives the
|
|
42
|
+
* tab. Collapsing the two back into one "clear it all" step reintroduces a window in which a failed
|
|
43
|
+
* request loses the work.
|
|
44
|
+
*/
|
|
45
|
+
/** The fields a standing draft carries — see `PostAutosaveSnapshot`'s own server-side doc for why
|
|
46
|
+
* `status` is deliberately absent (a draft must never change whether a row is live). */
|
|
47
|
+
export type StandingDraftAutosaveInput = {
|
|
48
|
+
bodyFormat: "doc";
|
|
49
|
+
bodyJson: Record<string, unknown>;
|
|
50
|
+
title: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
baseVersion: number;
|
|
53
|
+
} | {
|
|
54
|
+
bodyFormat: "html";
|
|
55
|
+
bodyHtml: string;
|
|
56
|
+
title: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
baseVersion: number;
|
|
59
|
+
};
|
|
60
|
+
export interface StandingDraftAutosaveSnapshot {
|
|
61
|
+
bodyFormat: "doc" | "html";
|
|
62
|
+
bodyJson?: Record<string, unknown>;
|
|
63
|
+
bodyHtml?: string;
|
|
64
|
+
title: string;
|
|
65
|
+
slug: string;
|
|
66
|
+
baseVersion: number;
|
|
67
|
+
savedAt: string;
|
|
68
|
+
savedByPrincipalId: string;
|
|
69
|
+
/** Fresh server timestamp used only for the explicit recovery comparison. */
|
|
70
|
+
serverUpdatedAt?: string | undefined;
|
|
71
|
+
}
|
|
72
|
+
/** What this hook needs from the outside world — see this file's header for why it is declared
|
|
73
|
+
* here rather than imported from either feature's own port. */
|
|
74
|
+
export interface StandingDraftAutosavePort {
|
|
75
|
+
/** Captured before expiry so late failures cannot borrow the next operator's identity. */
|
|
76
|
+
getBackupPrincipalId?: (requiredArgs: Record<string, never>, optionalArgs?: Record<string, never>) => string | null;
|
|
77
|
+
/**
|
|
78
|
+
* `options.keepalive` (2026-09-07) asks for an UNLOAD-SAFE transport, and is set only by the exit
|
|
79
|
+
* flush below. An ordinary `fetch` started from `pagehide` or `visibilitychange`→hidden is
|
|
80
|
+
* cancelled when the browser tears the document down — so the newest edit, the one still sitting
|
|
81
|
+
* in the debounce window, was precisely the one those listeners could not actually rescue.
|
|
82
|
+
*
|
|
83
|
+
* Opt-in rather than always-on because `keepalive` carries a 64 KiB body ceiling an ordinary
|
|
84
|
+
* request does not; an implementation that cannot honour it may ignore it (the parameter is
|
|
85
|
+
* optional), which is exactly the behavior this path had before.
|
|
86
|
+
*/
|
|
87
|
+
putAutosave(required: {
|
|
88
|
+
id: string;
|
|
89
|
+
draft: StandingDraftAutosaveInput;
|
|
90
|
+
}, options?: {
|
|
91
|
+
keepalive?: boolean;
|
|
92
|
+
}): Promise<{
|
|
93
|
+
applied: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
getAutosave(required: {
|
|
96
|
+
id: string;
|
|
97
|
+
}, optional?: Record<string, never>): Promise<{
|
|
98
|
+
autosave: StandingDraftAutosaveSnapshot | null;
|
|
99
|
+
}>;
|
|
100
|
+
discardAutosave(required: {
|
|
101
|
+
id: string;
|
|
102
|
+
}, optional?: Record<string, never>): Promise<{
|
|
103
|
+
ok: boolean;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
/** A write the server refused because the row moved on — see this file's header. Carries the
|
|
107
|
+
* operator's refused text itself, not just a flag: it is the copy nothing else on the client has
|
|
108
|
+
* a claim to, and a conflict notice that drops the work would be worse than the silent drop. */
|
|
109
|
+
export interface StandingDraftStaleBasis {
|
|
110
|
+
/** The `baseVersion` the refused draft was built on. The gate lifts when a later
|
|
111
|
+
* `scheduleAutosave` arrives on a different one. */
|
|
112
|
+
baseVersion: number;
|
|
113
|
+
/** Exactly what was refused. */
|
|
114
|
+
draft: StandingDraftAutosaveInput;
|
|
115
|
+
}
|
|
116
|
+
export interface StandingDraftAutosaveController {
|
|
117
|
+
/** Non-null once the mount-time recovery check resolves and finds a parked draft. Stays exactly
|
|
118
|
+
* as read until {@link dismissRecoverable}/{@link clearStandingDraft} clears it — never
|
|
119
|
+
* overwritten by a later `scheduleAutosave` tick from THIS session, since a session that is
|
|
120
|
+
* actively autosaving has, by definition, already accounted for whatever it found on mount. */
|
|
121
|
+
recoverableDraft: StandingDraftAutosaveSnapshot | null;
|
|
122
|
+
/** Non-null once the server has refused a write for this entry, and until the caller edits on a
|
|
123
|
+
* fresh basis (or a real Save/discard runs). While it is set, `scheduleAutosave` deliberately
|
|
124
|
+
* sends nothing — every such write would be refused. The editor should tell the operator that
|
|
125
|
+
* another save happened, that reloading is what resumes autosaving, and that their text is kept.
|
|
126
|
+
* Purely additive: a caller that ignores it behaves exactly as before EXCEPT that refused writes
|
|
127
|
+
* are no longer retried forever. */
|
|
128
|
+
staleBasis: StandingDraftStaleBasis | null;
|
|
129
|
+
/** Clears `recoverableDraft` locally only (no server call) — the "Discard" affordance calls
|
|
130
|
+
* {@link clearStandingDraft} instead, which also tells the server; this is for a caller that
|
|
131
|
+
* wants to stop showing the banner without discarding anything (e.g. "Restore" applied it into
|
|
132
|
+
* the editor, and a real Save moments later will clear the server-side draft on its own). */
|
|
133
|
+
dismissRecoverable: () => void;
|
|
134
|
+
/** Call on every content change the caller wants protected — cheap, debounced internally
|
|
135
|
+
* (idle 3s / 15s hard ceiling while continuously typing), safe to call on every keystroke.
|
|
136
|
+
* Whatever is still sitting in the debounce window when the editor goes away is written by the
|
|
137
|
+
* flush this hook wires up itself — see the `flushPendingAutosave` effects below. */
|
|
138
|
+
scheduleAutosave: (draft: StandingDraftAutosaveInput) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Clears the server-side standing draft — call after a real Save/Publish succeeds, or when the
|
|
141
|
+
* operator explicitly discards a recovered draft. Cancels any pending (not yet fired) debounce
|
|
142
|
+
* timer FIRST, then enqueues the discard onto the same serial request chain every
|
|
143
|
+
* `scheduleAutosave` tick uses, so a still-in-flight (already fired) autosave write is guaranteed
|
|
144
|
+
* to reach the server before this discard does, and no new one can be queued after it — the
|
|
145
|
+
* ordering guarantee that keeps a late autosave from resurrecting content a real save (or an
|
|
146
|
+
* explicit discard) just superseded.
|
|
147
|
+
*/
|
|
148
|
+
clearStandingDraft: () => Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* The caller replaced its basis with a newer saved row it did not write itself (an assistant
|
|
151
|
+
* write applied by `use-external-entry-refresh.hooks.ts`, silently or through Load latest). Every
|
|
152
|
+
* write built on an OLDER `baseVersion` is superseded from here on: a pending one is cancelled, a
|
|
153
|
+
* later `scheduleAutosave` on one is dropped (a render that committed before the replacement can
|
|
154
|
+
* still run its autosave effect after this call), and the refusal of one already in flight is
|
|
155
|
+
* ignored. Without this, that write reached the server, was refused, paused autosaving and
|
|
156
|
+
* mirrored text for a version the editor had already left (reviewer finding 2, 2026-09-16).
|
|
157
|
+
*
|
|
158
|
+
* A gate recorded on an older basis lifts, because the editor is no longer on it. That refusal's
|
|
159
|
+
* mirror is kept: nothing has accepted its text anywhere (see this file's header). Does not touch
|
|
160
|
+
* the server-side draft — Load latest also calls {@link clearStandingDraft} for that.
|
|
161
|
+
*/
|
|
162
|
+
supersedeBasis: (baseVersion: number) => void;
|
|
163
|
+
preserveFailedSave: (requiredArgs: {
|
|
164
|
+
error: unknown;
|
|
165
|
+
draft: StandingDraftAutosaveInput;
|
|
166
|
+
}, optionalArgs?: Record<string, never>) => void;
|
|
167
|
+
}
|
|
168
|
+
export interface StandingDraftAutosaveOptions {
|
|
169
|
+
port: StandingDraftAutosavePort;
|
|
170
|
+
backup: ReturnType<typeof createStandingDraftLocalBackup>;
|
|
171
|
+
/** `null` before the entry has loaded (or for a not-yet-created entry) — every operation no-ops. */
|
|
172
|
+
entryId: string | null;
|
|
173
|
+
/** Gate for "this editor is in a state where autosave should run at all" (e.g. not while a save
|
|
174
|
+
* or delete is already in flight). Recovery-check and scheduling both respect it. */
|
|
175
|
+
enabled: boolean;
|
|
176
|
+
serverUpdatedAt?: string | undefined;
|
|
177
|
+
}
|
|
178
|
+
export declare function useStandingDraftAutosave(deps: StandingDraftAutosaveOptions, _options?: Record<string, never>): StandingDraftAutosaveController;
|
|
179
|
+
//# sourceMappingURL=use-standing-draft-autosave.hooks.d.ts.map
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useSerialWrites } from "./use-serial-writes.hooks.js";
|
|
3
|
+
/** Idle debounce: fires this long after the LAST `scheduleAutosave` call with no further calls. */
|
|
4
|
+
const IDLE_DEBOUNCE_MS = 3000;
|
|
5
|
+
/** Hard ceiling while continuously typing: once this much time has passed since the first
|
|
6
|
+
* unflushed edit, the next tick fires immediately instead of resetting the idle window again. */
|
|
7
|
+
const MAX_WAIT_MS = 15000;
|
|
8
|
+
export function useStandingDraftAutosave(deps, _options = {}) {
|
|
9
|
+
const { port, backup, entryId, enabled, serverUpdatedAt } = deps;
|
|
10
|
+
const [backupPrincipalId] = useState(() => port.getBackupPrincipalId?.({}, {}));
|
|
11
|
+
const latestDraftRef = useRef(null);
|
|
12
|
+
const backupGenerationRef = useRef(0);
|
|
13
|
+
const [recoverableDraft, setRecoverableDraft] = useState(null);
|
|
14
|
+
const [staleBasis, setStaleBasis] = useState(null);
|
|
15
|
+
// Mirrors `staleBasis` for the SYNCHRONOUS gate in `scheduleAutosave`: the refusal is discovered
|
|
16
|
+
// inside the request chain, and a React state read there would be a render behind — the window in
|
|
17
|
+
// which the very ticks this gate exists to stop would still be scheduled.
|
|
18
|
+
const staleBasisRef = useRef(null);
|
|
19
|
+
// Which entry's refused text is currently mirrored in browser storage, or `null`. Deliberately
|
|
20
|
+
// NOT the same lifetime as the gate above (see this file's header), and deliberately an entry id
|
|
21
|
+
// rather than a boolean: it is the proof that a mirror this hook may drop belongs to the entry
|
|
22
|
+
// being edited right now, so switching entries can never delete another entry's surviving copy.
|
|
23
|
+
const backupEntryIdRef = useRef(null);
|
|
24
|
+
// Every network op (each autosave PUT, and the eventual discard) is appended here, never fired
|
|
25
|
+
// standalone — this IS the ordering guarantee `clearStandingDraft`'s own doc describes. Caught
|
|
26
|
+
// internally (see `enqueue`) so one failed request can never permanently wedge the lane.
|
|
27
|
+
const writes = useSerialWrites();
|
|
28
|
+
const idleTimerRef = useRef(null);
|
|
29
|
+
const firstPendingAtRef = useRef(null);
|
|
30
|
+
// The newest draft handed to `scheduleAutosave` that has NOT yet been written — what a flush
|
|
31
|
+
// sends. A ref, not the timer's own closure, precisely so an exit event can reach it: the
|
|
32
|
+
// closure is unreachable from outside the `setTimeout` callback, which is why cancelling the
|
|
33
|
+
// timer used to be indistinguishable from losing the edit.
|
|
34
|
+
const pendingDraftRef = useRef(null);
|
|
35
|
+
// The lowest `baseVersion` a write may still carry for the current entry — see `supersedeBasis`.
|
|
36
|
+
// `null` until the caller supersedes a basis; reset on an entry change, because another entry's
|
|
37
|
+
// version numbers are unrelated.
|
|
38
|
+
const minBaseVersionRef = useRef(null);
|
|
39
|
+
const clearPendingTimer = useCallback(() => {
|
|
40
|
+
if (idleTimerRef.current !== null)
|
|
41
|
+
clearTimeout(idleTimerRef.current);
|
|
42
|
+
idleTimerRef.current = null;
|
|
43
|
+
firstPendingAtRef.current = null;
|
|
44
|
+
pendingDraftRef.current = null;
|
|
45
|
+
}, []);
|
|
46
|
+
/** The server refused this draft. Records the basis, and mirrors the text locally — the ONLY
|
|
47
|
+
* place it can survive a tab close while the server keeps refusing it. @complexity O(1). */
|
|
48
|
+
const markStaleBasis = useCallback((id, draft) => {
|
|
49
|
+
const next = { baseVersion: draft.baseVersion, draft };
|
|
50
|
+
staleBasisRef.current = next;
|
|
51
|
+
setStaleBasis(next);
|
|
52
|
+
backup.write({ entryId: id, draft, savedAt: new Date().toISOString() }, { principalId: backupPrincipalId });
|
|
53
|
+
backupEntryIdRef.current = id;
|
|
54
|
+
}, [backupPrincipalId, backup]);
|
|
55
|
+
/** Lifts the gate so scheduling resumes. Does NOT touch the mirror — see this file's header for
|
|
56
|
+
* why those two are separate. No-op when nothing is stale, so the common path never re-renders.
|
|
57
|
+
* @complexity O(1). */
|
|
58
|
+
const liftStaleGate = useCallback(() => {
|
|
59
|
+
if (staleBasisRef.current === null)
|
|
60
|
+
return;
|
|
61
|
+
staleBasisRef.current = null;
|
|
62
|
+
setStaleBasis(null);
|
|
63
|
+
}, []);
|
|
64
|
+
/** Drops the browser-storage mirror for the entry being edited, if it has one. Called only where
|
|
65
|
+
* the operator's text provably survives somewhere else — the server just accepted a write (so it
|
|
66
|
+
* is parked server-side), or a real Save/discard superseded it. @complexity O(1). */
|
|
67
|
+
const dropLocalBackup = useCallback(() => {
|
|
68
|
+
const id = backupEntryIdRef.current;
|
|
69
|
+
if (id === null)
|
|
70
|
+
return;
|
|
71
|
+
backupEntryIdRef.current = null;
|
|
72
|
+
backup.clear({ entryId: id }, { principalId: backupPrincipalId });
|
|
73
|
+
}, [backupPrincipalId, backup]);
|
|
74
|
+
// Mount-time (and entry-change) recovery check — one read, never repeated while this entryId
|
|
75
|
+
// stays mounted. A stale in-flight response from a PREVIOUS entryId is dropped via `cancelled`.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setRecoverableDraft(null);
|
|
78
|
+
// A different entry is a different basis. A gate recorded for the PREVIOUS entryId must never
|
|
79
|
+
// silence autosave for this one — their `version` numbers are very often both 1, so leaving it
|
|
80
|
+
// set would silently stop autosaving an entry nobody has a conflict on. In-memory only: the
|
|
81
|
+
// previous entry's local mirror is keyed under ITS id and stays recoverable there.
|
|
82
|
+
staleBasisRef.current = null;
|
|
83
|
+
setStaleBasis(null);
|
|
84
|
+
// Same reasoning for the mirror pointer, one step further: the previous entry's mirror stays in
|
|
85
|
+
// storage under ITS OWN key and stays recoverable there — only this hook's permission to drop a
|
|
86
|
+
// mirror is reset, so a later accepted write for THIS entry cannot delete that other copy.
|
|
87
|
+
backupEntryIdRef.current = null;
|
|
88
|
+
minBaseVersionRef.current = null;
|
|
89
|
+
latestDraftRef.current = null;
|
|
90
|
+
if (!enabled || !entryId)
|
|
91
|
+
return;
|
|
92
|
+
let cancelled = false;
|
|
93
|
+
port
|
|
94
|
+
.getAutosave({ id: entryId }, {})
|
|
95
|
+
.then((result) => {
|
|
96
|
+
if (cancelled)
|
|
97
|
+
return;
|
|
98
|
+
const localBackup = backup.read({ entryId }, { principalId: backupPrincipalId });
|
|
99
|
+
// Recorded whether or not it is the copy shown below: it exists, so a real Save or an
|
|
100
|
+
// explicit discard on this entry has to be able to drop it. Without this the mirror written
|
|
101
|
+
// before a reload would outlive every discard and re-offer itself on every later mount.
|
|
102
|
+
if (localBackup !== null)
|
|
103
|
+
backupEntryIdRef.current = entryId;
|
|
104
|
+
// Historically server first — a parked draft is shared across tabs. For expiry recovery,
|
|
105
|
+
// compare capture times instead: a 401 mirror may contain edits newer than that parked copy. The local
|
|
106
|
+
// mirror is the fallback for exactly the case that produced it: the other tab's real save
|
|
107
|
+
// cleared the server-side draft, so this tab reloads into "nothing to recover" while its
|
|
108
|
+
// own refused text is the thing the operator actually wants back.
|
|
109
|
+
const server = result.autosave;
|
|
110
|
+
// A 401 mirror can be newer than the last accepted server autosave. Keep the newest copy.
|
|
111
|
+
const newest = localBackup && (!server || localBackup.savedAt > server.savedAt) ? localBackup : server;
|
|
112
|
+
setRecoverableDraft(newest);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => {
|
|
115
|
+
// A failed server recovery read must not hide the local copy rescued from an expired session.
|
|
116
|
+
if (cancelled)
|
|
117
|
+
return;
|
|
118
|
+
const localBackup = backup.read({ entryId }, { principalId: backupPrincipalId });
|
|
119
|
+
if (localBackup !== null)
|
|
120
|
+
backupEntryIdRef.current = entryId;
|
|
121
|
+
setRecoverableDraft(localBackup);
|
|
122
|
+
});
|
|
123
|
+
return () => {
|
|
124
|
+
cancelled = true;
|
|
125
|
+
};
|
|
126
|
+
}, [enabled, entryId, port, backupPrincipalId, backup]);
|
|
127
|
+
const enqueue = useCallback((op) => writes.run({ task: op }).catch(() => {
|
|
128
|
+
// eslint-disable-next-line no-console -- best-effort background persistence; surfaced to
|
|
129
|
+
// the console rather than the operator, who has nothing actionable to do about one dropped
|
|
130
|
+
// autosave tick, and rather than thrown, which would wedge every later tick on this chain.
|
|
131
|
+
console.error("standing-draft autosave: request failed");
|
|
132
|
+
}), [writes]);
|
|
133
|
+
const preserveFailedSave = useCallback(({ error, draft }, _options = {}) => {
|
|
134
|
+
if (!entryId || !isUnauthorizedDraftWrite(error))
|
|
135
|
+
return;
|
|
136
|
+
backup.write({ entryId, draft, savedAt: new Date().toISOString() }, { principalId: backupPrincipalId });
|
|
137
|
+
backupEntryIdRef.current = entryId;
|
|
138
|
+
}, [entryId, backupPrincipalId, backup]);
|
|
139
|
+
/** The one place `putAutosave` is called. Its `{ applied }` answer is acted on here rather than
|
|
140
|
+
* discarded — see this file's STALE BASIS note. @complexity Time/space: O(1) plus the request. */
|
|
141
|
+
const writeAutosave = useCallback((id, draft, keepalive) => {
|
|
142
|
+
const generation = backupGenerationRef.current;
|
|
143
|
+
return enqueue(async () => {
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await port.putAutosave({ id, draft }, { keepalive });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// A 401 can unmount the editor before this settles; the captured identity still owns it.
|
|
150
|
+
// Use the newest scheduled text, since an older request may fail while typing continues.
|
|
151
|
+
if (generation === backupGenerationRef.current)
|
|
152
|
+
preserveFailedSave({ error, draft: latestDraftRef.current ?? draft });
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
if (generation !== backupGenerationRef.current)
|
|
156
|
+
return;
|
|
157
|
+
if (!result.applied) {
|
|
158
|
+
// Refused because the caller already moved past this basis — nothing to pause or mirror.
|
|
159
|
+
if (isSupersededBasis(draft.baseVersion, minBaseVersionRef.current))
|
|
160
|
+
return;
|
|
161
|
+
markStaleBasis(id, draft);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Accepted — the text is parked server-side now, which is the ONLY point at which the
|
|
165
|
+
// mirror is redundant and may go.
|
|
166
|
+
liftStaleGate();
|
|
167
|
+
dropLocalBackup();
|
|
168
|
+
});
|
|
169
|
+
}, [dropLocalBackup, enqueue, liftStaleGate, markStaleBasis, port, preserveFailedSave]);
|
|
170
|
+
const scheduleAutosave = useCallback((draft) => {
|
|
171
|
+
if (!enabled || !entryId)
|
|
172
|
+
return;
|
|
173
|
+
latestDraftRef.current = draft;
|
|
174
|
+
if (isSupersededBasis(draft.baseVersion, minBaseVersionRef.current))
|
|
175
|
+
return;
|
|
176
|
+
// The gate. Same basis as the one the server already refused => this write would be refused
|
|
177
|
+
// too, so it is not scheduled at all; a DIFFERENT basis means the editor reloaded the row, so
|
|
178
|
+
// the gate lifts by itself and no caller has to remember to reset anything.
|
|
179
|
+
const stale = staleBasisRef.current;
|
|
180
|
+
if (stale !== null && stale.baseVersion === draft.baseVersion)
|
|
181
|
+
return;
|
|
182
|
+
// The mirror is deliberately left in place here: nothing has been written yet, so until the
|
|
183
|
+
// server accepts, that mirror is still the only copy of the refused text that outlives the tab.
|
|
184
|
+
liftStaleGate();
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
if (firstPendingAtRef.current === null)
|
|
187
|
+
firstPendingAtRef.current = now;
|
|
188
|
+
const waitedSoFar = now - firstPendingAtRef.current;
|
|
189
|
+
// Clamped, not a plain "already past the ceiling?" check: without the clamp, a call arriving
|
|
190
|
+
// just before the ceiling would still schedule a FULL fresh `IDLE_DEBOUNCE_MS`, pushing the
|
|
191
|
+
// actual fire time past `MAX_WAIT_MS` under continuous typing (verified by the ceiling test —
|
|
192
|
+
// an unclamped version never fires within the window it claims to bound).
|
|
193
|
+
const delay = Math.max(0, Math.min(IDLE_DEBOUNCE_MS, MAX_WAIT_MS - waitedSoFar));
|
|
194
|
+
pendingDraftRef.current = draft;
|
|
195
|
+
if (idleTimerRef.current !== null)
|
|
196
|
+
clearTimeout(idleTimerRef.current);
|
|
197
|
+
idleTimerRef.current = setTimeout(() => {
|
|
198
|
+
idleTimerRef.current = null;
|
|
199
|
+
firstPendingAtRef.current = null;
|
|
200
|
+
pendingDraftRef.current = null;
|
|
201
|
+
// Not an exit: the page is still here, so an ordinary fetch (no 64 KiB ceiling) is right.
|
|
202
|
+
writeAutosave(entryId, draft, false);
|
|
203
|
+
}, delay);
|
|
204
|
+
}, [enabled, entryId, liftStaleGate, writeAutosave]);
|
|
205
|
+
/**
|
|
206
|
+
* Writes a pending (not yet fired) autosave immediately instead of waiting the debounce out —
|
|
207
|
+
* the half of the cadence that makes the owner's actual requirement true ("what if they reload
|
|
208
|
+
* the page on accident or exit out of the page? I don't want them to lose their work"). The
|
|
209
|
+
* debounce always leaves a window of up to {@link IDLE_DEBOUNCE_MS} in which the newest edit
|
|
210
|
+
* exists only in React state; without this, every exit inside that window discards it.
|
|
211
|
+
*
|
|
212
|
+
* Goes onto the same serial chain every other write uses, so it can never overtake an in-flight
|
|
213
|
+
* PUT or a discard already queued by {@link clearStandingDraft} — and because that function
|
|
214
|
+
* nulls `pendingDraftRef`, a flush after a real Save is a no-op rather than a resurrection.
|
|
215
|
+
*
|
|
216
|
+
* @complexity Time/space: O(1) — one ref read and at most one enqueued request.
|
|
217
|
+
*/
|
|
218
|
+
const flushPendingAutosave = useCallback((keepalive) => {
|
|
219
|
+
const draft = pendingDraftRef.current;
|
|
220
|
+
if (draft === null || !entryId)
|
|
221
|
+
return;
|
|
222
|
+
clearPendingTimer();
|
|
223
|
+
writeAutosave(entryId, draft, keepalive);
|
|
224
|
+
}, [clearPendingTimer, entryId, writeAutosave]);
|
|
225
|
+
// Flush, do not cancel, when this editor goes away. An in-app navigation (the "Posts"/"Pages"
|
|
226
|
+
// back link, or any router move) unmounts the hook while a debounced tick is still pending, and
|
|
227
|
+
// the previous cancel-on-unmount is exactly how the last seconds of typing were lost — verified
|
|
228
|
+
// in a real browser on 2026-09-06: typed into the Pages editor, followed the in-app link one
|
|
229
|
+
// second later, and no PUT was ever issued. The SPA outlives the unmount, so the request started
|
|
230
|
+
// here completes normally. Re-running on an `entryId` change is correct too: the cleanup closes
|
|
231
|
+
// over the OUTGOING entry, so switching entries parks the one being left behind.
|
|
232
|
+
// `keepalive: false` — an unmount is an in-app navigation, not a page teardown. The SPA outlives
|
|
233
|
+
// it, so the request started here completes normally and must not pay keepalive's body ceiling.
|
|
234
|
+
useEffect(() => () => flushPendingAutosave(false), [flushPendingAutosave]);
|
|
235
|
+
// The exits React never sees. `pagehide` covers a tab close, a reload and a same-tab navigation;
|
|
236
|
+
// `visibilitychange` covers a tab switch or the OS backgrounding the browser, where `pagehide`
|
|
237
|
+
// may never fire; `blur` covers switching to another desktop app with the tab still visible.
|
|
238
|
+
// `beforeunload` is deliberately NOT one of them — `use-dirty-guard.hooks.ts` already registers
|
|
239
|
+
// it, and it is a PROMPT, not a persistence hook: answering "Leave" still discards whatever the
|
|
240
|
+
// debounce had not written. Each handler is a no-op when nothing is pending, so overlapping
|
|
241
|
+
// events (a close fires blur AND pagehide) cost one write, not three.
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (!enabled || !entryId)
|
|
244
|
+
return;
|
|
245
|
+
// Wrapped rather than passed straight to `addEventListener`: a listener is called WITH the
|
|
246
|
+
// Event, which would arrive as `flushPendingAutosave`'s `keepalive` argument — a truthy object,
|
|
247
|
+
// so every flush would silently request keepalive whether or not it is an exit.
|
|
248
|
+
const onExit = () => flushPendingAutosave(true);
|
|
249
|
+
const onVisibilityChange = () => {
|
|
250
|
+
if (document.visibilityState === "hidden")
|
|
251
|
+
flushPendingAutosave(true);
|
|
252
|
+
};
|
|
253
|
+
window.addEventListener("pagehide", onExit);
|
|
254
|
+
window.addEventListener("blur", onExit);
|
|
255
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
256
|
+
return () => {
|
|
257
|
+
window.removeEventListener("pagehide", onExit);
|
|
258
|
+
window.removeEventListener("blur", onExit);
|
|
259
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
260
|
+
};
|
|
261
|
+
}, [enabled, entryId, flushPendingAutosave]);
|
|
262
|
+
const clearStandingDraft = useCallback(async () => {
|
|
263
|
+
// A late 401/refusal must not resurrect a draft a successful Save or Discard superseded.
|
|
264
|
+
backupGenerationRef.current++;
|
|
265
|
+
clearPendingTimer();
|
|
266
|
+
setRecoverableDraft(null);
|
|
267
|
+
// A real Save landed (or the operator discarded): whatever was refused is superseded, so the
|
|
268
|
+
// gate lifts and the mirror goes with it — otherwise the next mount would offer a ghost. This
|
|
269
|
+
// is also the ONLY thing that clears a mirror the operator has already been shown and chose to
|
|
270
|
+
// discard, since by then the gate that wrote it belongs to a previous mount.
|
|
271
|
+
liftStaleGate();
|
|
272
|
+
dropLocalBackup();
|
|
273
|
+
if (!entryId)
|
|
274
|
+
return;
|
|
275
|
+
await enqueue(async () => {
|
|
276
|
+
await port.discardAutosave({ id: entryId }, {});
|
|
277
|
+
});
|
|
278
|
+
}, [clearPendingTimer, dropLocalBackup, enqueue, entryId, liftStaleGate, port]);
|
|
279
|
+
const dismissRecoverable = useCallback(() => setRecoverableDraft(null), []);
|
|
280
|
+
const supersedeBasis = useCallback((baseVersion) => {
|
|
281
|
+
minBaseVersionRef.current = baseVersion;
|
|
282
|
+
const pending = pendingDraftRef.current;
|
|
283
|
+
if (pending !== null && isSupersededBasis(pending.baseVersion, baseVersion))
|
|
284
|
+
clearPendingTimer();
|
|
285
|
+
const stale = staleBasisRef.current;
|
|
286
|
+
if (stale !== null && isSupersededBasis(stale.baseVersion, baseVersion))
|
|
287
|
+
liftStaleGate();
|
|
288
|
+
}, [clearPendingTimer, liftStaleGate]);
|
|
289
|
+
const comparedDraft = useMemo(() => recoverableDraft ? { ...recoverableDraft, serverUpdatedAt } : null, [recoverableDraft, serverUpdatedAt]);
|
|
290
|
+
return { recoverableDraft: comparedDraft, staleBasis, dismissRecoverable, scheduleAutosave, clearStandingDraft, supersedeBasis, preserveFailedSave };
|
|
291
|
+
}
|
|
292
|
+
/** Whether a write built on `baseVersion` belongs to a basis the caller has left (see
|
|
293
|
+
* `supersedeBasis`). @complexity O(1). */
|
|
294
|
+
function isSupersededBasis(baseVersion, minBaseVersion) {
|
|
295
|
+
return minBaseVersion !== null && baseVersion < minBaseVersion;
|
|
296
|
+
}
|
|
297
|
+
function isUnauthorizedDraftWrite(error) {
|
|
298
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 401;
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=use-standing-draft-autosave.hooks.js.map
|
|
@@ -13,7 +13,13 @@ export { resolveActiveTabId } from './helpers/resolve-active-tab-id.js';
|
|
|
13
13
|
export { hasPermission } from './helpers/permissions.js';
|
|
14
14
|
export { isAbortError, retryWhileUnreachable, UNREACHABLE_RETRY_DELAYS_MS } from './helpers/retry-unreachable.js';
|
|
15
15
|
export { buildAgentListHandles } from '@jini-ai/agentic';
|
|
16
|
-
export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural } from '../i18n/index.js';
|
|
16
|
+
export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural, localeEntry } from '../i18n/index.js';
|
|
17
17
|
export type { Translate, DictionaryTranslator, LocaleDictionary } from '../i18n/index.js';
|
|
18
18
|
export type { DirtyGuardHostPort } from './hooks/use-dirty-guard.hooks.js';
|
|
19
|
+
export { useSettingsSlice, mergeSaveStates, commitQueuedSave, canAcceptRefresh, SAVE_DEBOUNCE_MS } from './hooks/use-settings-slice.hooks.js';
|
|
20
|
+
export type { SaveState, SettingsSlice, SettingsSliceOptions, SettingsSliceRefreshPort, SettingsRefreshScope } from './hooks/use-settings-slice.hooks.js';
|
|
21
|
+
export { useStandingDraftAutosave } from './hooks/use-standing-draft-autosave.hooks.js';
|
|
22
|
+
export type { StandingDraftAutosavePort, StandingDraftAutosaveInput, StandingDraftAutosaveSnapshot, StandingDraftAutosaveController, StandingDraftStaleBasis, StandingDraftAutosaveOptions } from './hooks/use-standing-draft-autosave.hooks.js';
|
|
23
|
+
export { createStandingDraftLocalBackup, LOCAL_BACKUP_PRINCIPAL_ID } from './helpers/standing-draft-local-backup.js';
|
|
24
|
+
export { standingDraftRecoveryMessage } from './helpers/standing-draft-recovery-message.js';
|
|
19
25
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -9,5 +9,9 @@ export { resolveActiveTabId } from './helpers/resolve-active-tab-id.js';
|
|
|
9
9
|
export { hasPermission } from './helpers/permissions.js';
|
|
10
10
|
export { isAbortError, retryWhileUnreachable, UNREACHABLE_RETRY_DELAYS_MS } from './helpers/retry-unreachable.js';
|
|
11
11
|
export { buildAgentListHandles } from '@jini-ai/agentic';
|
|
12
|
-
export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural } from '../i18n/index.js';
|
|
12
|
+
export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural, localeEntry } from '../i18n/index.js';
|
|
13
|
+
export { useSettingsSlice, mergeSaveStates, commitQueuedSave, canAcceptRefresh, SAVE_DEBOUNCE_MS } from './hooks/use-settings-slice.hooks.js';
|
|
14
|
+
export { useStandingDraftAutosave } from './hooks/use-standing-draft-autosave.hooks.js';
|
|
15
|
+
export { createStandingDraftLocalBackup, LOCAL_BACKUP_PRINCIPAL_ID } from './helpers/standing-draft-local-backup.js';
|
|
16
|
+
export { standingDraftRecoveryMessage } from './helpers/standing-draft-recovery-message.js';
|
|
13
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,41 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* overlaps" section flagged "Resource-dashboard shell, twice, both directly
|
|
6
|
-
* Run-vocabulary-relevant": `DesignsTab.tsx` (sub-tab sort + search +
|
|
7
|
-
* select-mode/bulk-delete + per-item kebab menu + a grid/kanban view toggle
|
|
8
|
-
* over a config-driven status-kanban board) and `TasksView.tsx` (hero header
|
|
9
|
-
* with metric tiles + a flat row-list with inline per-item action buttons +
|
|
10
|
-
* a lazy-loaded expandable per-row run-history sublist). Both were read in
|
|
11
|
-
* full from `/tmp/od-source` (see `packages/ui/archived provenance ledger`'s
|
|
12
|
-
* `features/resource-dashboard/` section for the full shared-vs-separate
|
|
13
|
-
* verdict and quoted structural evidence).
|
|
14
|
-
*
|
|
15
|
-
* Verdict: genuinely TWO distinct composition shapes, not one. The only
|
|
16
|
-
* piece that is actually identical between the two origins is "a status
|
|
17
|
-
* value renders as a small badge" plus "group a list of status-bearing
|
|
18
|
-
* items into ordered buckets" — DesignsTab uses that grouping to build a
|
|
19
|
-
* whole-page kanban board; TasksView never groups by status at all (its
|
|
20
|
-
* only status usage is a per-row/per-run `StatusPill`, one level deeper
|
|
21
|
-
* than DesignsTab's top-level items). So this feature ships ONE shared
|
|
22
|
-
* status vocabulary + `StatusPill` + `groupItemsByStatus` rule, consumed by
|
|
23
|
-
* TWO separately composed components: `ResourceBoard` (the DesignsTab
|
|
24
|
-
* shape) and `ResourceRowList` (the TasksView shape). Neither is generic
|
|
25
|
-
* over the other; forcing them through one composed shell would mean
|
|
26
|
-
* bolting kanban-grouping onto a shape that never uses it, or bolting
|
|
27
|
-
* expandable nested history onto a shape that never had it.
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* A host-supplied status value + its display label. Never a hardcoded
|
|
31
|
-
* vocabulary: DesignsTab's own 6-value kanban vocabulary
|
|
32
|
-
* (`not_started`/`running`/`awaiting_input`/`succeeded`/`failed`/`canceled`)
|
|
33
|
-
* and TasksView's own 5-value run vocabulary
|
|
34
|
-
* (`succeeded`/`failed`/`running`/`queued`/`canceled`) are two different,
|
|
35
|
-
* non-identical instantiations of this same shape — see
|
|
36
|
-
* `packages/ui/archived provenance ledger` for the note on reconciling either against
|
|
37
|
-
* `@jini-ai/protocol`'s own `RunState` later (explicitly out of scope here).
|
|
2
|
+
* ResourceBoard and ResourceRowList share status badges/vocabulary and ordered grouping rules,
|
|
3
|
+
* but compose separate surfaces: a grid/kanban board versus rows with inline actions and nested
|
|
4
|
+
* lazy history. A combined shell would impose unused grouping or history on the other shape.
|
|
38
5
|
*/
|
|
6
|
+
/** Host-supplied status and label. Board and row/run vocabularies can differ; reconciling them
|
|
7
|
+
* with protocol RunState is a separate decision, not implicit in this display primitive. */
|
|
39
8
|
export interface ResourceStatusOption {
|
|
40
9
|
value: string;
|
|
41
10
|
label: string;
|
|
@@ -63,10 +32,7 @@ export interface ResourceMenuActionSpec {
|
|
|
63
32
|
* option needs (DesignsTab's `recent` sorts by `updatedAt`, `yours` by
|
|
64
33
|
* `createdAt` — two different fields on the same origin `Project` type) —
|
|
65
34
|
* this primitive never assumes a single fixed timestamp field.
|
|
66
|
-
* `body` is a host-supplied render slot
|
|
67
|
-
* cover-thumbnail resolution across html/image/video/logo/brand kinds stays
|
|
68
|
-
* entirely host-owned, never ported into this generic primitive — see
|
|
69
|
-
* archived provenance ledger's "Dropped" list).
|
|
35
|
+
* `body` is a host-supplied render slot; content-kind-specific thumbnail resolution is host-owned.
|
|
70
36
|
*/
|
|
71
37
|
export interface ResourceBoardItem<TBody = unknown> {
|
|
72
38
|
id: string;
|
|
@@ -110,7 +76,8 @@ export interface ResourceRowItem {
|
|
|
110
76
|
paused?: boolean;
|
|
111
77
|
actions: ResourceRowAction[];
|
|
112
78
|
}
|
|
113
|
-
/**
|
|
79
|
+
/** Entry in an expandable run-history list. Action kinds are host-owned; the primitive does
|
|
80
|
+
* not implement a product's automation-evolution workflow. */
|
|
114
81
|
export interface ResourceRunHistoryItem {
|
|
115
82
|
id: string;
|
|
116
83
|
status: string;
|
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* overlaps" section flagged "Resource-dashboard shell, twice, both directly
|
|
6
|
-
* Run-vocabulary-relevant": `DesignsTab.tsx` (sub-tab sort + search +
|
|
7
|
-
* select-mode/bulk-delete + per-item kebab menu + a grid/kanban view toggle
|
|
8
|
-
* over a config-driven status-kanban board) and `TasksView.tsx` (hero header
|
|
9
|
-
* with metric tiles + a flat row-list with inline per-item action buttons +
|
|
10
|
-
* a lazy-loaded expandable per-row run-history sublist). Both were read in
|
|
11
|
-
* full from `/tmp/od-source` (see `packages/ui/archived provenance ledger`'s
|
|
12
|
-
* `features/resource-dashboard/` section for the full shared-vs-separate
|
|
13
|
-
* verdict and quoted structural evidence).
|
|
14
|
-
*
|
|
15
|
-
* Verdict: genuinely TWO distinct composition shapes, not one. The only
|
|
16
|
-
* piece that is actually identical between the two origins is "a status
|
|
17
|
-
* value renders as a small badge" plus "group a list of status-bearing
|
|
18
|
-
* items into ordered buckets" — DesignsTab uses that grouping to build a
|
|
19
|
-
* whole-page kanban board; TasksView never groups by status at all (its
|
|
20
|
-
* only status usage is a per-row/per-run `StatusPill`, one level deeper
|
|
21
|
-
* than DesignsTab's top-level items). So this feature ships ONE shared
|
|
22
|
-
* status vocabulary + `StatusPill` + `groupItemsByStatus` rule, consumed by
|
|
23
|
-
* TWO separately composed components: `ResourceBoard` (the DesignsTab
|
|
24
|
-
* shape) and `ResourceRowList` (the TasksView shape). Neither is generic
|
|
25
|
-
* over the other; forcing them through one composed shell would mean
|
|
26
|
-
* bolting kanban-grouping onto a shape that never uses it, or bolting
|
|
27
|
-
* expandable nested history onto a shape that never had it.
|
|
2
|
+
* ResourceBoard and ResourceRowList share status badges/vocabulary and ordered grouping rules,
|
|
3
|
+
* but compose separate surfaces: a grid/kanban board versus rows with inline actions and nested
|
|
4
|
+
* lazy history. A combined shell would impose unused grouping or history on the other shape.
|
|
28
5
|
*/
|
|
29
6
|
export {};
|
|
30
7
|
//# sourceMappingURL=types.js.map
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Drag feedback duration in milliseconds. */
|
|
2
2
|
export declare const TAB_STRIP_DRAG_HAPTIC_MS = 8;
|
|
3
|
-
/**
|
|
3
|
+
/** Drop feedback duration in milliseconds. */
|
|
4
4
|
export declare const TAB_STRIP_DROP_HAPTIC_MS = 12;
|
|
5
|
-
/** DOM attribute used for strip-level drop-target hit-testing
|
|
6
|
-
* form of `WorkspaceTabsBar.tsx`'s `data-workspace-tab-id`). */
|
|
5
|
+
/** DOM attribute used for strip-level drop-target hit-testing. */
|
|
7
6
|
export declare const TAB_STRIP_ITEM_ID_ATTRIBUTE = "data-tab-strip-item-id";
|
|
8
7
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Drag feedback duration in milliseconds. */
|
|
2
2
|
export const TAB_STRIP_DRAG_HAPTIC_MS = 8;
|
|
3
|
-
/**
|
|
3
|
+
/** Drop feedback duration in milliseconds. */
|
|
4
4
|
export const TAB_STRIP_DROP_HAPTIC_MS = 12;
|
|
5
|
-
/** DOM attribute used for strip-level drop-target hit-testing
|
|
6
|
-
* form of `WorkspaceTabsBar.tsx`'s `data-workspace-tab-id`). */
|
|
5
|
+
/** DOM attribute used for strip-level drop-target hit-testing. */
|
|
7
6
|
export const TAB_STRIP_ITEM_ID_ATTRIBUTE = 'data-tab-strip-item-id';
|
|
8
7
|
//# sourceMappingURL=constants.js.map
|