@k2b/cloud 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -2
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -5
- package/src/ai/inference-calls.ts +5 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +21 -44
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +27 -22
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +35 -5
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +6 -1
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +1 -1
- package/src/server/api/respond.ts +17 -4
- package/src/server/index.ts +2 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- package/src/services/app-access.ts +25 -7
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/session/index.ts +26 -3
- package/src/services/settings/core-settings.ts +71 -43
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +3 -1
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import * as T from "@k2b/ui/text-editor/model";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { captureTextAnchor, resolveTextAnchor } from "../shared-text/anchors";
|
|
4
|
+
import { captureTableFocus, resolveTableFocus, type TableFocusWitness } from "../shared-text/table-actions";
|
|
5
|
+
|
|
6
|
+
export type SharedTextInverseRequest = {
|
|
7
|
+
action: "undo" | "redo";
|
|
8
|
+
receiptCursor: string;
|
|
9
|
+
actionId: string;
|
|
10
|
+
applied: Promise<void>;
|
|
11
|
+
cancel: (reason: unknown) => void;
|
|
12
|
+
};
|
|
13
|
+
type NativeFrame = Y.UndoManager["undoStack"][number];
|
|
14
|
+
type CellBoundary = { focus: TableFocusWitness; left: Uint8Array | null; right: Uint8Array };
|
|
15
|
+
type Boundary = { left: Uint8Array | null; right: Uint8Array | null; cell?: CellBoundary };
|
|
16
|
+
type TextFrame = {
|
|
17
|
+
kind: "text";
|
|
18
|
+
item: NativeFrame;
|
|
19
|
+
boundaries: Boundary[];
|
|
20
|
+
liveInsertions: NativeFrame["insertions"];
|
|
21
|
+
baseline: Uint8Array | null;
|
|
22
|
+
};
|
|
23
|
+
type Frame = TextFrame | { kind: "structure"; cursor: string };
|
|
24
|
+
|
|
25
|
+
// UndoManager deliberately skips effectless native frames. A remote structural
|
|
26
|
+
// deletion can make an earlier typing frame effectless; reject it before a
|
|
27
|
+
// native call can silently consume the next independent step as well.
|
|
28
|
+
const hasUncovered = (wanted: NativeFrame["insertions"], excluded: NativeFrame["insertions"]): boolean => {
|
|
29
|
+
for (const [client, ranges] of wanted.clients) {
|
|
30
|
+
const removed = excluded.clients.get(client) ?? [];
|
|
31
|
+
for (const range of ranges) {
|
|
32
|
+
let clock = range.clock;
|
|
33
|
+
for (const deletion of removed) {
|
|
34
|
+
if (deletion.clock + deletion.len <= clock) continue;
|
|
35
|
+
if (deletion.clock > clock) break;
|
|
36
|
+
clock = Math.max(clock, deletion.clock + deletion.len);
|
|
37
|
+
if (clock >= range.clock + range.len) break;
|
|
38
|
+
}
|
|
39
|
+
if (clock < range.clock + range.len) return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The supplied manager owns ordinary input only. Structural receipts are remote
|
|
47
|
+
* and their inverses are authorized by the server. Hosts route every history
|
|
48
|
+
* entry point through this controller, including native editor key bindings.
|
|
49
|
+
*/
|
|
50
|
+
export const createSharedTextActionHistory = (doc: Y.Doc, history: Y.UndoManager, onChange?: () => void, onRetainedColumn?: () => void) => {
|
|
51
|
+
const undo: Frame[] = [],
|
|
52
|
+
redo: Frame[] = [];
|
|
53
|
+
const remote = {};
|
|
54
|
+
const text = doc.getText("codemirror");
|
|
55
|
+
let deletedBoundaries: Boundary[] = [];
|
|
56
|
+
// One public text observer captures surviving identities, before the native
|
|
57
|
+
// manager emits stack-item-added/updated in afterTransaction. Metadata lives
|
|
58
|
+
// only on reachable frames, and disappears with clear/dispose.
|
|
59
|
+
const observe = (event: Y.YTextEvent) => {
|
|
60
|
+
deletedBoundaries = [];
|
|
61
|
+
let at = 0;
|
|
62
|
+
// Content neighbors may be removed/restored by another native manager.
|
|
63
|
+
// Relax only inside the SAME still-live physical cell scaffold, never for
|
|
64
|
+
// an operation deleting table syntax (including delimiter escapes) or a
|
|
65
|
+
// vanished row/column. Reviving a backslash can escape a still-live pipe.
|
|
66
|
+
// Y.Text's changes.deleted is always empty. Its public transaction delete
|
|
67
|
+
// set is the actual deleted content, before transaction cleanup/GC.
|
|
68
|
+
let contentOnly = true;
|
|
69
|
+
Y.iterateDeletedStructs(event.transaction, event.transaction.deleteSet, (item) => {
|
|
70
|
+
if (!(item instanceof Y.Item)) {
|
|
71
|
+
contentOnly = false;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (item.parent !== text) return;
|
|
75
|
+
const content = item.content;
|
|
76
|
+
if (!(content instanceof Y.ContentString) || ["|", "\\", "\n", "\r"].some((value) => content.str.includes(value)))
|
|
77
|
+
contentOnly = false;
|
|
78
|
+
});
|
|
79
|
+
const tables = contentOnly && event.delta.some((delta) => delta.delete) ? T.findTables(text.toString()) : [];
|
|
80
|
+
const captureCell = (at: number): CellBoundary | undefined => {
|
|
81
|
+
for (const table of tables) {
|
|
82
|
+
const index = table.lines.findIndex((line) => at >= line.from && at <= line.to);
|
|
83
|
+
if (index < 0 || index === 1) continue;
|
|
84
|
+
const line = table.lines[index]!;
|
|
85
|
+
for (let column = 0; column < table.columns; column++) {
|
|
86
|
+
const left = line.leadingPipe ? line.pipes[column] : column === 0 ? line.from - 1 : line.pipes[column - 1];
|
|
87
|
+
const right = (line.leadingPipe ? line.pipes[column + 1] : line.pipes[column]) ?? line.to;
|
|
88
|
+
if (left === undefined || at <= left || at > right || right >= text.length) continue;
|
|
89
|
+
try {
|
|
90
|
+
const row = index === 0 ? 0 : index - 1;
|
|
91
|
+
return {
|
|
92
|
+
focus: captureTableFocus(text, {
|
|
93
|
+
tableFrom: table.from,
|
|
94
|
+
rowFrom: line.from,
|
|
95
|
+
columnFrom: table.lines[0]!.cells[column]!.from,
|
|
96
|
+
row,
|
|
97
|
+
column,
|
|
98
|
+
}),
|
|
99
|
+
left: left < 0 ? null : captureTextAnchor(text, left),
|
|
100
|
+
right: captureTextAnchor(text, right),
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
};
|
|
109
|
+
for (const delta of event.delta) {
|
|
110
|
+
if (delta.retain) {
|
|
111
|
+
at += delta.retain;
|
|
112
|
+
}
|
|
113
|
+
if (delta.insert) at += typeof delta.insert === "string" ? delta.insert.length : 1;
|
|
114
|
+
if (delta.delete) {
|
|
115
|
+
deletedBoundaries.push({
|
|
116
|
+
left: at > 0 ? captureTextAnchor(text, at - 1) : null,
|
|
117
|
+
right: at < text.length ? captureTextAnchor(text, at) : null,
|
|
118
|
+
cell: captureCell(at),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
text.observe(observe);
|
|
124
|
+
const available = (boundary: Boundary, frame: TextFrame) => {
|
|
125
|
+
const left = boundary.left ? resolveTextAnchor(text, boundary.left) : -1;
|
|
126
|
+
const right = boundary.right ? resolveTextAnchor(text, boundary.right) : text.length;
|
|
127
|
+
const removedHere = (bytes: Uint8Array | null) => {
|
|
128
|
+
const id = bytes && Y.decodeRelativePosition(bytes).item;
|
|
129
|
+
return !!id && Y.isDeleted(frame.item.deletions, id);
|
|
130
|
+
};
|
|
131
|
+
// Later edits merged into this SAME native frame may delete its earlier
|
|
132
|
+
// boundary. Their newer outer boundary still protects the whole reversal.
|
|
133
|
+
if ((left === null && !removedHere(boundary.left)) || (right === null && !removedHere(boundary.right))) {
|
|
134
|
+
const cell = boundary.cell;
|
|
135
|
+
const tables = cell ? T.findTables(text.toString()) : [];
|
|
136
|
+
let target = cell && resolveTableFocus(text, cell.focus);
|
|
137
|
+
if (cell && !target) {
|
|
138
|
+
// Inserting a column on the left adds a delimiter BEFORE the original
|
|
139
|
+
// hyphen; it does not replace that column. Native content history can
|
|
140
|
+
// follow this insertion only while the original hyphen/right edge and
|
|
141
|
+
// strict table/row witnesses survive. Server action witnesses stay fixed.
|
|
142
|
+
const hyphen = resolveTextAnchor(text, cell.focus.column.hyphen);
|
|
143
|
+
const oldLeft = resolveTextAnchor(text, cell.focus.column.left);
|
|
144
|
+
for (const table of tables) {
|
|
145
|
+
const separator = table.lines[1]!;
|
|
146
|
+
const column = separator.cells.findIndex((cell) => hyphen !== null && hyphen >= cell.from && hyphen < cell.to);
|
|
147
|
+
if (column < 0) continue;
|
|
148
|
+
const left = separator.leadingPipe ? separator.pipes[column] : column === 0 ? table.lines[0]!.to : separator.pipes[column - 1];
|
|
149
|
+
if (
|
|
150
|
+
left === undefined ||
|
|
151
|
+
oldLeft === null ||
|
|
152
|
+
oldLeft > left ||
|
|
153
|
+
(oldLeft !== table.lines[0]!.to && !separator.pipes.includes(oldLeft))
|
|
154
|
+
)
|
|
155
|
+
continue;
|
|
156
|
+
target = resolveTableFocus(text, {
|
|
157
|
+
...cell.focus,
|
|
158
|
+
column: { ...cell.focus.column, left: captureTextAnchor(text, left) },
|
|
159
|
+
});
|
|
160
|
+
if (target) break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const tableFrom = target?.tableFrom;
|
|
164
|
+
const table = target && tables.find((table) => table.from === tableFrom);
|
|
165
|
+
const line = table && target && T.gridLine(table, target.row);
|
|
166
|
+
if (!cell || !target || !line) return false;
|
|
167
|
+
const edgeLeft = line.leadingPipe ? line.pipes[target.column] : target.column === 0 ? line.from - 1 : line.pipes[target.column - 1];
|
|
168
|
+
const edgeRight = (line.leadingPipe ? line.pipes[target.column + 1] : line.pipes[target.column]) ?? line.to;
|
|
169
|
+
const oldLeft = cell.left ? resolveTextAnchor(text, cell.left) : -1;
|
|
170
|
+
if (
|
|
171
|
+
edgeLeft === undefined ||
|
|
172
|
+
oldLeft === null ||
|
|
173
|
+
oldLeft < line.from - 1 ||
|
|
174
|
+
oldLeft > edgeLeft ||
|
|
175
|
+
(oldLeft !== line.from - 1 && !line.pipes.includes(oldLeft)) ||
|
|
176
|
+
resolveTextAnchor(text, cell.right) !== edgeRight
|
|
177
|
+
)
|
|
178
|
+
return false;
|
|
179
|
+
// A deleted neighbor's ordinary fallback must still lie INSIDE that
|
|
180
|
+
// original column and row. Merely finding a same-looking cell is unsafe.
|
|
181
|
+
return [boundary.left, boundary.right].every((bytes) => {
|
|
182
|
+
if (!bytes) return false;
|
|
183
|
+
const position = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(bytes), doc, false);
|
|
184
|
+
return position?.type === text && position.index > edgeLeft && position.index <= edgeRight;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (left !== null && right !== null && right <= left) return false;
|
|
188
|
+
if (boundary.left !== null || boundary.right !== null || !text.length) return true;
|
|
189
|
+
// Whole-text replacements have no external boundaries. They are safe only
|
|
190
|
+
// while all live replacement identities still belong to this frame.
|
|
191
|
+
const first = Y.createRelativePositionFromTypeIndex(text, 0).item;
|
|
192
|
+
if (!first) return false;
|
|
193
|
+
let item: Y.Item | null = Y.getItem(doc.store, first);
|
|
194
|
+
while (item) {
|
|
195
|
+
if (
|
|
196
|
+
!item.deleted &&
|
|
197
|
+
item.countable &&
|
|
198
|
+
!frame.liveInsertions.clients
|
|
199
|
+
.get(item.id.client)
|
|
200
|
+
?.some((run) => run.clock <= item!.id.clock && run.clock + run.len >= item!.id.clock + item!.length)
|
|
201
|
+
)
|
|
202
|
+
return false;
|
|
203
|
+
item = item.right;
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
};
|
|
207
|
+
const includesRun = (set: NativeFrame["insertions"], id: Y.ID, length = 1) =>
|
|
208
|
+
set.clients.get(id.client)?.some((run) => run.clock <= id.clock && run.clock + run.len >= id.clock + length) ?? false;
|
|
209
|
+
const restoredAt = (id: Y.ID, created: NativeFrame["insertions"]) => {
|
|
210
|
+
const at = (assoc: number) => Y.createAbsolutePositionFromRelativePosition(new Y.RelativePosition(null, null, id, assoc), doc, true);
|
|
211
|
+
const start = at(0),
|
|
212
|
+
end = at(-1);
|
|
213
|
+
// A deleted item's ordinary fallback resolves both associations to the same
|
|
214
|
+
// gap. A real live native restoration resolves on either SIDE of its char.
|
|
215
|
+
if (start?.type !== text || end?.type !== text || end.index !== start.index + 1) return null;
|
|
216
|
+
const live = Y.createRelativePositionFromTypeIndex(text, start.index).item;
|
|
217
|
+
return live && includesRun(created, live) ? { id: live, index: start.index } : null;
|
|
218
|
+
};
|
|
219
|
+
const refreshRestorations = (created: NativeFrame["insertions"]) => {
|
|
220
|
+
if (!created.clients.size) return;
|
|
221
|
+
for (const frame of [...undo, ...redo]) {
|
|
222
|
+
if (frame.kind !== "text") continue;
|
|
223
|
+
for (const boundary of frame.boundaries)
|
|
224
|
+
for (const side of ["left", "right"] as const) {
|
|
225
|
+
const bytes = boundary[side];
|
|
226
|
+
if (!bytes || resolveTextAnchor(text, bytes) !== null) continue;
|
|
227
|
+
const id = Y.decodeRelativePosition(bytes).item;
|
|
228
|
+
const restored = id && restoredAt(id, created);
|
|
229
|
+
if (restored) boundary[side] = captureTextAnchor(text, restored.index);
|
|
230
|
+
}
|
|
231
|
+
const next = Y.createDeleteSet();
|
|
232
|
+
for (const [client, ranges] of frame.liveInsertions.clients) {
|
|
233
|
+
for (const range of ranges) {
|
|
234
|
+
let clock = range.clock;
|
|
235
|
+
const until = clock + range.len;
|
|
236
|
+
while (clock < until) {
|
|
237
|
+
const item = Y.getItem(doc.store, Y.createID(client, clock));
|
|
238
|
+
const length = Math.min(until - clock, item.id.clock + item.length - clock);
|
|
239
|
+
const first = restoredAt(Y.createID(client, clock), created);
|
|
240
|
+
const last = restoredAt(Y.createID(client, clock + length - 1), created);
|
|
241
|
+
const mapped =
|
|
242
|
+
first &&
|
|
243
|
+
last &&
|
|
244
|
+
first.id.client === last.id.client &&
|
|
245
|
+
last.id.clock === first.id.clock + length - 1 &&
|
|
246
|
+
includesRun(created, first.id, length);
|
|
247
|
+
const id = mapped ? first.id : Y.createID(client, clock);
|
|
248
|
+
const target = next.clients.get(id.client) ?? [];
|
|
249
|
+
target.push({ clock: id.clock, len: length });
|
|
250
|
+
next.clients.set(id.client, target);
|
|
251
|
+
clock += length;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
frame.liveInsertions = Y.mergeDeleteSets([next]);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
let disposed = false;
|
|
259
|
+
let notifying = false;
|
|
260
|
+
// Native events can run inside yCollab's EditorView.update. Host callbacks
|
|
261
|
+
// may reconfigure that view through reactive UI state; never call them there.
|
|
262
|
+
const notify = () => {
|
|
263
|
+
if (notifying || disposed) return;
|
|
264
|
+
notifying = true;
|
|
265
|
+
queueMicrotask(() => {
|
|
266
|
+
notifying = false;
|
|
267
|
+
if (!disposed) onChange?.();
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
let moving = false;
|
|
271
|
+
let inverse: ((request: SharedTextInverseRequest) => Promise<void>) | undefined;
|
|
272
|
+
let pending: { id: string; resolve(): void; reject(reason: unknown): void; direction?: "undo" | "redo"; frame?: Frame } | null = null;
|
|
273
|
+
const added = ({ stackItem }: { stackItem: NativeFrame }) => {
|
|
274
|
+
if (moving) return;
|
|
275
|
+
undo.push({
|
|
276
|
+
kind: "text",
|
|
277
|
+
item: stackItem,
|
|
278
|
+
boundaries: [...deletedBoundaries],
|
|
279
|
+
liveInsertions: stackItem.insertions,
|
|
280
|
+
baseline: Y.encodeStateVector(doc),
|
|
281
|
+
});
|
|
282
|
+
redo.length = 0;
|
|
283
|
+
notify();
|
|
284
|
+
};
|
|
285
|
+
const updated = ({ stackItem }: { stackItem: NativeFrame }) => {
|
|
286
|
+
if (moving) return;
|
|
287
|
+
const frame = undo.findLast((frame) => frame.kind === "text" && frame.item === stackItem);
|
|
288
|
+
if (frame?.kind === "text") {
|
|
289
|
+
frame.boundaries.push(...deletedBoundaries);
|
|
290
|
+
frame.liveInsertions = stackItem.insertions;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const cleared = ({ undoStackCleared, redoStackCleared }: { undoStackCleared: boolean; redoStackCleared: boolean }) => {
|
|
294
|
+
if (moving) return;
|
|
295
|
+
if (undoStackCleared) undo.length = 0;
|
|
296
|
+
if (redoStackCleared) redo.length = 0;
|
|
297
|
+
notify();
|
|
298
|
+
};
|
|
299
|
+
history.on("stack-item-added", added);
|
|
300
|
+
history.on("stack-item-updated", updated);
|
|
301
|
+
history.on("stack-cleared", cleared);
|
|
302
|
+
// Existing input frames have no captured deletion context. Such restoration
|
|
303
|
+
// is unavailable. Insertion-only Undo requires no pre-registration redone
|
|
304
|
+
// chains; validation refuses unknown live correspondence before mutation.
|
|
305
|
+
undo.push(
|
|
306
|
+
...history.undoStack.map((item) => ({ kind: "text" as const, item, boundaries: [], liveInsertions: item.insertions, baseline: null })),
|
|
307
|
+
);
|
|
308
|
+
const cancel = (id: string, reason: unknown) => {
|
|
309
|
+
if (pending?.id !== id) return;
|
|
310
|
+
const action = pending;
|
|
311
|
+
pending = null;
|
|
312
|
+
action.reject(reason);
|
|
313
|
+
notify();
|
|
314
|
+
};
|
|
315
|
+
const begin = (id: string, direction?: "undo" | "redo", frame?: Frame) => {
|
|
316
|
+
if (disposed || pending || !id) throw new Error("A table action is unavailable or still awaiting its receipt");
|
|
317
|
+
const applied = new Promise<void>((resolve, reject) => {
|
|
318
|
+
pending = { id, resolve, reject, direction, frame };
|
|
319
|
+
});
|
|
320
|
+
void applied.catch(() => {});
|
|
321
|
+
notify();
|
|
322
|
+
return { applied, cancel: (reason: unknown) => cancel(id, reason) };
|
|
323
|
+
};
|
|
324
|
+
const move = async (direction: "undo" | "redo") => {
|
|
325
|
+
if (disposed || pending) return null;
|
|
326
|
+
const from = direction === "undo" ? undo : redo,
|
|
327
|
+
to = direction === "undo" ? redo : undo;
|
|
328
|
+
const frame = from.at(-1);
|
|
329
|
+
if (!frame) return null;
|
|
330
|
+
history.stopCapturing();
|
|
331
|
+
if (frame.kind === "structure") {
|
|
332
|
+
if (!inverse) throw new Error("Structural history requires an authorized inverse handler");
|
|
333
|
+
const actionId = crypto.randomUUID();
|
|
334
|
+
const action = begin(actionId, direction, frame);
|
|
335
|
+
try {
|
|
336
|
+
await inverse({ action: direction, receiptCursor: frame.cursor, actionId, ...action });
|
|
337
|
+
await action.applied;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
action.cancel(error);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
return frame;
|
|
343
|
+
}
|
|
344
|
+
const { validateNativeHistory } = await import("./shared-text-history-validation");
|
|
345
|
+
if (disposed || pending || from.at(-1) !== frame) throw new Error("The input history frame is unavailable");
|
|
346
|
+
// Public UndoManager events preserve CodeMirror selection metadata. Never
|
|
347
|
+
// pop another input frame if the referenced native step became unavailable.
|
|
348
|
+
const stack = direction === "undo" ? history.undoStack : history.redoStack;
|
|
349
|
+
if (stack.at(-1) !== frame.item) throw new Error("The input history frame is unavailable");
|
|
350
|
+
if (
|
|
351
|
+
hasUncovered(frame.item.deletions, frame.item.insertions) &&
|
|
352
|
+
(!frame.boundaries.length || frame.boundaries.some((boundary) => !available(boundary, frame)))
|
|
353
|
+
)
|
|
354
|
+
throw new Error("The input history target is unavailable");
|
|
355
|
+
if (
|
|
356
|
+
!hasUncovered(frame.liveInsertions, Y.createDeleteSetFromStructStore(doc.store)) &&
|
|
357
|
+
!hasUncovered(frame.item.deletions, frame.item.insertions)
|
|
358
|
+
)
|
|
359
|
+
throw new Error("The input history frame is unavailable");
|
|
360
|
+
validateNativeHistory(text, frame.item, frame.liveInsertions, frame.baseline);
|
|
361
|
+
moving = true;
|
|
362
|
+
try {
|
|
363
|
+
const result = direction === "undo" ? history.undo() : history.redo();
|
|
364
|
+
if (!result) throw new Error("The input history frame could not be reversed");
|
|
365
|
+
from.pop();
|
|
366
|
+
const replacement = (direction === "undo" ? history.redoStack : history.undoStack).at(-1);
|
|
367
|
+
if (replacement) {
|
|
368
|
+
to.push({
|
|
369
|
+
kind: "text",
|
|
370
|
+
item: replacement,
|
|
371
|
+
boundaries: [...deletedBoundaries],
|
|
372
|
+
liveInsertions: replacement.insertions,
|
|
373
|
+
baseline: frame.baseline,
|
|
374
|
+
});
|
|
375
|
+
refreshRestorations(replacement.insertions);
|
|
376
|
+
}
|
|
377
|
+
notify();
|
|
378
|
+
return frame;
|
|
379
|
+
} finally {
|
|
380
|
+
moving = false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
isRemoteOrigin: (value: unknown) => value === remote,
|
|
385
|
+
hasPending: () => pending !== null,
|
|
386
|
+
canUndo: () => !pending && undo.length > 0,
|
|
387
|
+
canRedo: () => !pending && redo.length > 0,
|
|
388
|
+
nextStructural: (direction: "undo" | "redo") => (direction === "undo" ? undo : redo).at(-1)?.kind === "structure",
|
|
389
|
+
undoDepth: () => undo.length,
|
|
390
|
+
redoDepth: () => redo.length,
|
|
391
|
+
undo: () => move("undo"),
|
|
392
|
+
redo: () => move("redo"),
|
|
393
|
+
setInverseHandler(handler: (request: SharedTextInverseRequest) => Promise<void>) {
|
|
394
|
+
inverse = handler;
|
|
395
|
+
},
|
|
396
|
+
begin(id: string) {
|
|
397
|
+
return begin(id);
|
|
398
|
+
},
|
|
399
|
+
apply(update: Uint8Array, actionId?: string, cursor?: string, retained = false) {
|
|
400
|
+
Y.decodeUpdate(update);
|
|
401
|
+
const action = pending?.id === actionId ? pending : null;
|
|
402
|
+
if (action && !cursor) throw new Error("A structural receipt requires its retained cursor");
|
|
403
|
+
// Probe causal coverage before mutating, so a receipt never resolves
|
|
404
|
+
// while its target predecessors are still missing from this browser.
|
|
405
|
+
if (action) {
|
|
406
|
+
const probe = new Y.Doc();
|
|
407
|
+
try {
|
|
408
|
+
Y.applyUpdate(probe, Y.encodeStateAsUpdate(doc));
|
|
409
|
+
Y.applyUpdate(probe, update);
|
|
410
|
+
if (probe.store.pendingStructs || probe.store.pendingDs) throw new Error("The table action is missing preceding updates");
|
|
411
|
+
} finally {
|
|
412
|
+
probe.destroy();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
history.stopCapturing();
|
|
416
|
+
Y.applyUpdate(doc, update, remote);
|
|
417
|
+
history.stopCapturing();
|
|
418
|
+
if (!action) return;
|
|
419
|
+
pending = null;
|
|
420
|
+
if (action.direction) {
|
|
421
|
+
const from = action.direction === "undo" ? undo : redo,
|
|
422
|
+
to = action.direction === "undo" ? redo : undo;
|
|
423
|
+
const index = from.indexOf(action.frame!);
|
|
424
|
+
if (index >= 0) from.splice(index, 1);
|
|
425
|
+
to.push({ kind: "structure", cursor: cursor! });
|
|
426
|
+
if (retained && action.direction === "undo") onRetainedColumn?.();
|
|
427
|
+
} else {
|
|
428
|
+
undo.push({ kind: "structure", cursor: cursor! });
|
|
429
|
+
redo.length = 0;
|
|
430
|
+
// New structural input invalidates native Redo too. Public clear keeps
|
|
431
|
+
// current typing Undo and releases its unreachable native metadata.
|
|
432
|
+
moving = true;
|
|
433
|
+
try {
|
|
434
|
+
history.clear(false, true);
|
|
435
|
+
} finally {
|
|
436
|
+
moving = false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
action.resolve();
|
|
440
|
+
notify();
|
|
441
|
+
},
|
|
442
|
+
cancelPending(reason: unknown) {
|
|
443
|
+
if (pending) cancel(pending.id, reason);
|
|
444
|
+
},
|
|
445
|
+
clear() {
|
|
446
|
+
if (pending) cancel(pending.id, new Error("The editor history was cleared"));
|
|
447
|
+
undo.length = redo.length = 0;
|
|
448
|
+
deletedBoundaries = [];
|
|
449
|
+
history.clear();
|
|
450
|
+
notify();
|
|
451
|
+
},
|
|
452
|
+
dispose() {
|
|
453
|
+
if (disposed) return;
|
|
454
|
+
disposed = true;
|
|
455
|
+
if (pending) cancel(pending.id, new Error("The editor closed before the table action was confirmed"));
|
|
456
|
+
text.unobserve(observe);
|
|
457
|
+
deletedBoundaries = [];
|
|
458
|
+
history.off("stack-item-added", added);
|
|
459
|
+
history.off("stack-item-updated", updated);
|
|
460
|
+
history.off("stack-cleared", cleared);
|
|
461
|
+
undo.length = redo.length = 0;
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { openSharedTextOffline, type SharedTextOfflineIdentity, type SharedTextOfflineSession } from "./shared-text-offline";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Capture the device's logout fence BEFORE asking the server to authorize a
|
|
5
|
+
* session. Opening touches epoch metadata only. No draft bytes escape until
|
|
6
|
+
* authorization succeeds and load rechecks the same epoch transactionally.
|
|
7
|
+
*/
|
|
8
|
+
export const openAuthorizedSharedTextOffline = async <Authorized>(
|
|
9
|
+
identity: SharedTextOfflineIdentity,
|
|
10
|
+
authorize: (signal: AbortSignal) => Promise<Authorized>,
|
|
11
|
+
signal?: AbortSignal,
|
|
12
|
+
): Promise<{ session: SharedTextOfflineSession; authorization: Authorized; state: Uint8Array | null; baseCursor: string | null }> => {
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
const session = await openSharedTextOffline(identity);
|
|
15
|
+
const stop = () => session.close();
|
|
16
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
17
|
+
try {
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
const authorization = await authorize(session.signal);
|
|
20
|
+
session.signal.throwIfAborted();
|
|
21
|
+
const pending = await session.loadPending();
|
|
22
|
+
return { session, authorization, state: pending?.state ?? null, baseCursor: pending?.baseCursor ?? null };
|
|
23
|
+
} catch (error) {
|
|
24
|
+
session.close();
|
|
25
|
+
throw error;
|
|
26
|
+
} finally {
|
|
27
|
+
signal?.removeEventListener("abort", stop);
|
|
28
|
+
}
|
|
29
|
+
};
|