@alwith/module-drive 0.0.0-stage → 0.1.0-rc.23
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/LICENSE +94 -0
- package/README.md +50 -2
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/desktop-provenance.json +34 -0
- package/dist/badge-react.d.ts +9 -0
- package/dist/badge-react.js +1 -0
- package/dist/badge.d.ts +10 -0
- package/dist/badge.js +1 -0
- package/dist/browse.d.ts +33 -0
- package/dist/browse.js +1 -0
- package/dist/content/chat-condense.d.ts +22 -0
- package/dist/content/chat-condense.js +1 -0
- package/dist/content/collab.d.ts +34 -0
- package/dist/content/collab.js +1 -0
- package/dist/content/comment-anchors.d.ts +23 -0
- package/dist/content/comment-anchors.js +2 -0
- package/dist/content/comment-decorations.d.ts +23 -0
- package/dist/content/comment-decorations.js +3 -0
- package/dist/content/comment-drawer.d.ts +44 -0
- package/dist/content/comment-drawer.js +1 -0
- package/dist/content/comment-mentions.d.ts +18 -0
- package/dist/content/comment-mentions.js +1 -0
- package/dist/content/comment-surface.d.ts +25 -0
- package/dist/content/comment-surface.js +1 -0
- package/dist/content/condense-task.d.ts +44 -0
- package/dist/content/condense-task.js +1 -0
- package/dist/content/condense.d.ts +20 -0
- package/dist/content/condense.js +1 -0
- package/dist/content/editor.d.ts +5 -0
- package/dist/content/editor.js +1 -0
- package/dist/content/file-actions.d.ts +43 -0
- package/dist/content/file-actions.js +1 -0
- package/dist/content/file-link.d.ts +4 -0
- package/dist/content/file-link.js +1 -0
- package/dist/content/frontmatter-panel.d.ts +7 -0
- package/dist/content/frontmatter-panel.js +1 -0
- package/dist/content/frontmatter.d.ts +9 -0
- package/dist/content/frontmatter.js +7 -0
- package/dist/content/host.d.ts +49 -0
- package/dist/content/host.js +1 -0
- package/dist/content/html-bridge.d.ts +1 -0
- package/dist/content/html-bridge.js +187 -0
- package/dist/content/html-comments.d.ts +5 -0
- package/dist/content/html-comments.js +1 -0
- package/dist/content/markdown-comments.d.ts +4 -0
- package/dist/content/markdown-comments.js +2 -0
- package/dist/content/mention-picker.d.ts +10 -0
- package/dist/content/mention-picker.js +1 -0
- package/dist/content/save-version.d.ts +2 -0
- package/dist/content/save-version.js +1 -0
- package/dist/content/translations.d.ts +2 -0
- package/dist/content/translations.js +1 -0
- package/dist/content/use-file-comments.d.ts +12 -0
- package/dist/content/use-file-comments.js +1 -0
- package/dist/content/yuplink.d.ts +11 -0
- package/dist/content/yuplink.js +2 -0
- package/dist/content.css +64 -0
- package/dist/content.d.ts +10 -0
- package/dist/content.js +1 -0
- package/dist/contract.d.ts +780 -0
- package/dist/contract.js +0 -0
- package/dist/core.d.ts +22 -0
- package/dist/core.js +1 -0
- package/dist/files.d.ts +7 -0
- package/dist/files.js +1 -0
- package/dist/global-search.d.ts +34 -0
- package/dist/global-search.js +1 -0
- package/dist/icons.d.ts +5 -0
- package/dist/icons.js +1 -0
- package/dist/launcher.d.ts +15 -0
- package/dist/launcher.js +2 -0
- package/dist/locales.d.ts +985 -0
- package/dist/locales.js +1 -0
- package/dist/mass-delete.d.ts +9 -0
- package/dist/mass-delete.js +1 -0
- package/dist/notifications.d.ts +14 -0
- package/dist/notifications.js +1 -0
- package/dist/pinned-docs.d.ts +14 -0
- package/dist/pinned-docs.js +1 -0
- package/dist/pinned.d.ts +12 -0
- package/dist/pinned.js +1 -0
- package/dist/react.d.ts +29 -0
- package/dist/react.js +1 -0
- package/dist/roots.d.ts +10 -0
- package/dist/roots.js +1 -0
- package/dist/settings.d.ts +14 -0
- package/dist/settings.js +1 -0
- package/dist/sharing.d.ts +12 -0
- package/dist/sharing.js +1 -0
- package/dist/status.d.ts +16 -0
- package/dist/status.js +1 -0
- package/dist/styles.css +458 -0
- package/dist/summary.d.ts +10 -0
- package/dist/summary.js +1 -0
- package/dist/tauri.d.ts +6 -0
- package/dist/tauri.js +1 -0
- package/dist/transfer-feed.d.ts +36 -0
- package/dist/transfer-feed.js +1 -0
- package/dist/ui.d.ts +96 -0
- package/dist/ui.js +1 -0
- package/package.json +117 -4
- package/rust/Cargo.toml +14 -0
- package/rust/src/contract.rs +366 -0
- package/rust/src/lib.rs +5 -0
- package/rust/src/ownership.rs +4 -0
- package/rust/src/service.rs +140 -0
- package/rust/src/sync.rs +1602 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
ALwith Developer and Redistribution License, Version 1.0
|
|
2
|
+
Copyright 2026 ALWITHAI PTE. LTD. All rights reserved, except as expressly granted below.
|
|
3
|
+
|
|
4
|
+
1. Scope
|
|
5
|
+
This license applies only to ALWITHAI PTE. LTD.'s ("ALwith") own material in
|
|
6
|
+
the npm package accompanying this license (the "Software"). It is a proprietary
|
|
7
|
+
license, not an open-source license. Third-party material is governed by its
|
|
8
|
+
respective licenses, as described in THIRD_PARTY_NOTICES.md. No restriction here
|
|
9
|
+
reduces rights granted by those licenses or by mandatory applicable law.
|
|
10
|
+
|
|
11
|
+
2. Permission to develop and distribute applications
|
|
12
|
+
Subject to this license, ALwith grants you a worldwide, non-exclusive, royalty-free
|
|
13
|
+
copyright license to use, reproduce, integrate and modify the supplied Software
|
|
14
|
+
to develop, test, host and operate your own applications, for personal,
|
|
15
|
+
internal business or commercial use. You may brand,
|
|
16
|
+
sell and distribute those applications, including the Software embedded in them,
|
|
17
|
+
to your customers and end users. No per-application registration, approval or
|
|
18
|
+
royalty is required by this software license.
|
|
19
|
+
|
|
20
|
+
You may bundle, minify, transform and otherwise adapt the supplied JavaScript,
|
|
21
|
+
styles and declarations for those applications. You may include the supplied
|
|
22
|
+
Software in an application source distribution when necessary to build or use
|
|
23
|
+
that application, provided it remains separately identified under this license.
|
|
24
|
+
You may combine it with separately licensed application code, including MPL-2.0
|
|
25
|
+
code, while complying with both licenses. Your own independent code remains yours;
|
|
26
|
+
this license does not require disclosure of your application source code.
|
|
27
|
+
|
|
28
|
+
Recipients may use the embedded Software to run the application and exercise the
|
|
29
|
+
permissions above under this same license. You may allow your contractors and
|
|
30
|
+
hosting providers to use it for those purposes. You must supply this license and
|
|
31
|
+
all applicable third-party notices with each redistribution (for example in an
|
|
32
|
+
included licenses directory or an accessible legal-notices page), identify your
|
|
33
|
+
modifications, and not impose terms that purport to change ALwith's license.
|
|
34
|
+
|
|
35
|
+
3. Rights reserved
|
|
36
|
+
No right to ALwith's unpublished source code, private repositories, trademarks,
|
|
37
|
+
or separately distributed Runtime binaries, modules or services is granted here.
|
|
38
|
+
You may not relicense the Software as your own framework or redistribute it as
|
|
39
|
+
a standalone SDK/package for others, except for private development/build caches
|
|
40
|
+
used for applications permitted by Section 2. No restriction is imposed on
|
|
41
|
+
independently developed competing applications or on third-party licensed code.
|
|
42
|
+
You must not misrepresent authorship or imply ALwith endorses your application.
|
|
43
|
+
|
|
44
|
+
4. Accounts and services
|
|
45
|
+
This license does not supply platform credentials, paid entitlements, hosting,
|
|
46
|
+
support, updates or a service-availability guarantee. Use of ALwith accounts,
|
|
47
|
+
pets, relay or other hosted services is separately subject to the applicable
|
|
48
|
+
service terms and authorization. Do not bypass service access controls or use
|
|
49
|
+
another person's credentials without authorization. Choosing or replacing an
|
|
50
|
+
authentication provider for your own application is not prohibited by this
|
|
51
|
+
software license. Distributing an Auth client does not authorize its recipients
|
|
52
|
+
to access another user's account, data or entitlements.
|
|
53
|
+
|
|
54
|
+
5. Lawful use and independent responsibility
|
|
55
|
+
You must not use the Software to commit or knowingly facilitate unlawful acts.
|
|
56
|
+
You are responsible for your application's operation, permissions, business
|
|
57
|
+
practices, data processing, content and compliance with applicable law. You must
|
|
58
|
+
not make warranties or commitments on ALwith's behalf. ALwith does not endorse
|
|
59
|
+
your application merely because it uses the Software or connects to ALwith.
|
|
60
|
+
These allocations do not remove either party's own mandatory legal obligations.
|
|
61
|
+
|
|
62
|
+
6. Disclaimer and limitation of liability
|
|
63
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE IS PROVIDED
|
|
64
|
+
"AS IS" AND "AS AVAILABLE", WITHOUT EXPRESS OR IMPLIED WARRANTIES, INCLUDING
|
|
65
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ACCURACY,
|
|
66
|
+
SECURITY OR UNINTERRUPTED OPERATION. AI OUTPUTS AND AUTOMATED ACTIONS MAY BE
|
|
67
|
+
INCORRECT OR HARMFUL; YOU MUST DETERMINE APPROPRIATE REVIEW AND SAFEGUARDS.
|
|
68
|
+
|
|
69
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, ALWITH AND ITS CONTRIBUTORS
|
|
70
|
+
ARE NOT LIABLE FOR DAMAGES ARISING FROM USE, MODIFICATION OR DISTRIBUTION OF
|
|
71
|
+
THE SOFTWARE, INCLUDING LOST PROFITS, LOST DATA, BUSINESS INTERRUPTION AND
|
|
72
|
+
INDIRECT OR CONSEQUENTIAL LOSS. NOTHING EXCLUDES OR LIMITS LIABILITY THAT
|
|
73
|
+
CANNOT LAWFULLY BE EXCLUDED OR LIMITED, INCLUDING APPLICABLE MANDATORY CONSUMER
|
|
74
|
+
RIGHTS AND LIABILITY FOR FRAUD, WILLFUL MISCONDUCT OR GROSS NEGLIGENCE WHERE
|
|
75
|
+
SUCH EXCLUSION OR LIMITATION IS PROHIBITED. THIS LICENSE DOES NOT EXCLUDE
|
|
76
|
+
CRIMINAL OR REGULATORY RESPONSIBILITY IMPOSED BY LAW.
|
|
77
|
+
|
|
78
|
+
7. Duration and termination
|
|
79
|
+
The grant for this version continues while you comply with this license. If
|
|
80
|
+
ALwith gives written notice of a material breach, your rights terminate if you
|
|
81
|
+
do not cure it within 30 days; no cure period authorizes continuing unlawful
|
|
82
|
+
conduct. On termination, cease new use and distribution of ALwith's proprietary
|
|
83
|
+
Software except as required by law. Independently compliant recipients retain
|
|
84
|
+
their own grants. Sections concerning reserved rights, notices, disclaimers and
|
|
85
|
+
mandatory-law exceptions survive as applicable. Third-party licenses are not
|
|
86
|
+
terminated by this provision.
|
|
87
|
+
|
|
88
|
+
8. Versions and severability
|
|
89
|
+
This license governs the accompanying version. A later license or publication
|
|
90
|
+
of source code does not retroactively replace this grant or revoke rights in
|
|
91
|
+
earlier lawfully obtained versions. Any additional open-source license must be
|
|
92
|
+
expressly identified by ALwith. If a provision is unenforceable, the remaining
|
|
93
|
+
provisions continue to the extent permitted by law. A separate written agreement
|
|
94
|
+
signed by you and ALwith may expressly override these terms for its stated scope.
|
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @alwith/module-drive
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pre-release shared Drive module. JavaScript and Rust ship in the same versioned artifact. The native engine is extracted from ALwith Desktop; `desktop-provenance.json` records the source revision and original file hashes. Desktop migration is deferred.
|
|
4
|
+
|
|
5
|
+
## Entry points
|
|
6
|
+
|
|
7
|
+
- `@alwith/module-drive`: generated protocol types, transport interface and controller.
|
|
8
|
+
- `@alwith/module-drive/react`: controller subscriptions, launcher/settings/status panels, permissions, badges, mass-delete confirmation, localization and pinned-entry helpers. Inject host shadcn controls and navigation.
|
|
9
|
+
- `@alwith/module-drive/content`: comments, Markdown/HTML integration, collaboration, frontmatter, versions, knowledge jobs, chat suggestions, cross-project move and file-link helpers.
|
|
10
|
+
- `@alwith/module-drive/tauri`: transport constructor; no dependency on a particular Tauri version.
|
|
11
|
+
- `@alwith/module-drive/styles.css`: scoped semantic-token styles.
|
|
12
|
+
- `rust/`: `alwith-drive` service and `drive-sync` engine. Cargo consumers use the installed package path, not a sibling application checkout.
|
|
13
|
+
|
|
14
|
+
## Host contract
|
|
15
|
+
|
|
16
|
+
Create one native `DriveService` for the application. Inject application data, a shared ownership registry, default local directory, reserved legacy application directories, device identity and a system browser opener. Forward `execute` and `subscribe` only to authorized application windows. Forward a fresh snapshot on broadcast lag. Keep credentials out of snapshots, logs and persisted frontend state. Restrict the explicit MCP/collaboration credential responses to the primary trusted window.
|
|
17
|
+
|
|
18
|
+
Subscribe before fetching the initial snapshot. `createDriveController` buffers this interval and reconciles generation/sequence gaps. A window's disposal stops its listener, not the application service. Call and await native `shutdown` before terminating the process. Keep the service alive until shutdown completes.
|
|
19
|
+
|
|
20
|
+
All native filesystem mutations that can intersect the Drive root must run through `with_filesystem` after host path authorization. Pass exact write destinations and removed source paths. It serializes native changes with complete sync rounds, checks indexed read-only/collaboration state, and records successful removal intent. This coarse coordinator can delay operations during a transfer. It is not an OS sandbox: external editors and agents use ordinary filesystem permissions.
|
|
21
|
+
|
|
22
|
+
New roots must be empty by default. Hosts may opt into `DriveService::with_existing_default_root()` to adopt existing files only at their configured default root. This does not move or delete files and does not relax ownership checks for registered roots or custom directories. Ownership is persisted by canonical root, account/server and host data directory; overlapping roots and reuse by another profile are rejected. Legacy clients do not participate in the registry: sharing a default directory requires the same server/account and strictly sequential operation, with the previous sync process fully stopped. There is no automatic credential, index or existing-profile migration.
|
|
23
|
+
|
|
24
|
+
Credentials are stored in a native atomic profile file (owner-only permissions on Unix); protect the containing app data directory on the host OS. Logout preserves local files. Revocation retains the extracted engine's cleanup/residual behavior. Use dedicated accounts and temporary directories when testing revocation.
|
|
25
|
+
|
|
26
|
+
## Current boundaries
|
|
27
|
+
|
|
28
|
+
This remains an integration candidate pending real service and multi-client acceptance. The panel provides separate `launcher`, `settings` and `status` modes; the host supplies native directory selection, navigation, settings routing and external opening. It uses the existing workspace rather than rendering a second tree. Download preference changes reload the engine while preserving pause state.
|
|
29
|
+
|
|
30
|
+
Opt into content integration by mounting `DriveContentProvider`, wrapping managed documents in `CommentSurface`, and merging `drivePreviewIntegrations` into the shared editor host. Supply `saveDocument` and track collaboration ownership. Route authorized collaborative atomic writes through `with_collaborative_filesystem`; ordinary writes remain subject to `with_filesystem`. Lock source editing separately from collaborative preview permissions. The content entry requires its documented optional Milkdown, Yjs and editor peer dependencies.
|
|
31
|
+
|
|
32
|
+
Mount `DriveMassDeleteDialog` in the primary window independently of panel visibility. `ChatCondenseEntry` takes host preference storage for its 30-day snooze; pass approved jobs to `DriveCondense.initialJob`. Keep dialog ownership outside transient context menus. Hosts choose their OS link scheme; avoid claiming another application's registered protocol.
|
|
33
|
+
|
|
34
|
+
Archive is disabled by default. Hosts obtain explicit consent, export dialogue through their existing conversation owner, and provide `expectedGeneration` with each upload. Native code rechecks consent, private-session exclusion, workspace markers and generation before filtering and uploading. Archive metadata contains hashes and status, not a second conversation store. Knowledge and MCP configuration are resolved for each explicit session workspace. Configure a skills directory before starting the native engine.
|
|
35
|
+
|
|
36
|
+
Removal intent is inherited in-memory engine state; crash-safe delete/move recovery is not provided. The shared service adds no database schema migration. The editor host must preserve dirty buffers and check expected file versions. Native workspace size limits still apply after downloads. Cloud-only files cannot be edited locally; collaborative files require the explicit owner adapter rather than ordinary source writes.
|
|
37
|
+
|
|
38
|
+
Generated JSON numeric fields use JavaScript numbers, matching the existing transport. Values outside JavaScript's safe integer range are not supported; remote identifiers preserve string variants.
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bun install
|
|
44
|
+
bun run bindings
|
|
45
|
+
bun test src
|
|
46
|
+
cargo test --manifest-path rust/Cargo.toml --workspace
|
|
47
|
+
bun run build
|
|
48
|
+
bun pm pack
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use the repository's `scripts/build-npm.ts` builder. Publish only after an explicit release decision and real native, multi-process and supported-platform validation.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
React is supplied by the host under the MIT license.
|
|
4
|
+
|
|
5
|
+
The `drive-sync` sources originate in ALwith Desktop. The source revision and file hashes are recorded in `desktop-provenance.json`. Distribution of ALwith code is governed by the included `LICENSE`.
|
|
6
|
+
|
|
7
|
+
Rust dependencies are resolved from the included Cargo manifests and lockfile. They retain their respective upstream licenses, including Tokio (MIT), Serde (MIT OR Apache-2.0), reqwest (MIT OR Apache-2.0), rusqlite (MIT), and bundled SQLite (public domain). No dependency's license is replaced by the ALwith license. Host application distributors must include the license texts required by their final bundled dependency graph.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceRepository": "alwith-desktop",
|
|
3
|
+
"revision": "ae5d036ea1bdaac421fc9913f975cf831a1c2af1",
|
|
4
|
+
"files": {
|
|
5
|
+
"Cargo.toml": "daa8564159b50c725a4ae1467dd07f9b9dd0c41a3ea72cfe54d645fc2ba101a6",
|
|
6
|
+
"src/archive.rs": "27d41b1d1240052e4a7e7916b35bb8df2e3351515dbf0fd78f261c3749a8ce4a",
|
|
7
|
+
"src/cleanup.rs": "6fb4eaf11ad4f8d7c4dd1e6227409c60713fc5c9d9dd4796251cc5d8306c719e",
|
|
8
|
+
"src/client.rs": "2c5b50d5d8661b8ddf394c30f7a04bc1c6d6e6d38e700947920c05b6adb50c90",
|
|
9
|
+
"src/delete_guard.rs": "17ba13071086b946f84fef831d96ff48fa53f3ec435086dfcfe150538534e3f7",
|
|
10
|
+
"src/engine.rs": "d2710b65b9858606d739b00d4c842149220ec6aa0a34d35f7867cd0538d65b8a",
|
|
11
|
+
"src/ignore.rs": "043216cf3aebddc810379fba0b36063ba0117e3d048e2fa3731e415614db2c41",
|
|
12
|
+
"src/index.rs": "0d904cc0b68f022b5964c882aa3d20ace3087bd4f40ea84b801001e6fe4d9763",
|
|
13
|
+
"src/knowledge.rs": "816ecc052b978cecc06c3c4cfda3e6653f63eab0c80268ab349691f065fe1402",
|
|
14
|
+
"src/lib.rs": "b267122049ef7cca5dd0148af3d06952eba9c00bfa32c78558b9f4bb4d08b0fd",
|
|
15
|
+
"src/naming.rs": "edfaebf0472b2eb75b31ac9996a6431a9e99deeb83bc78420fa7c1dca860abf0",
|
|
16
|
+
"src/push.rs": "4c0a763682dda8ae3bf4d87b1bb40ec577d138ad0992130ed5de437afd868d1c",
|
|
17
|
+
"src/scanner.rs": "3f7b0d1a93e5a909b1aaa9b62e12473aacea997666f069e9326f6e2dbfe9df98",
|
|
18
|
+
"src/skills.rs": "6371c03047c7631d06de61154f009a1a54bed63fbb47dde338302c453d990125",
|
|
19
|
+
"src/sso.rs": "3932ed67869ec8c2f92d8c789ad4b1e4c84ed98b9087c8ccdc4691bd95550462",
|
|
20
|
+
"src/types.rs": "b3dd139e60346b54ff70086ca996c8d6fd004844daa8f833c7aa18c53f6182da",
|
|
21
|
+
"src/watcher.rs": "d267ebfa6cd1cb0b58bd0bc696237aabda495ce3a9f58877c6ada3a5c44975d6",
|
|
22
|
+
"tests/archive.rs": "b16d776513903e3c334b2f71202f5cc1384d319ff5ee55e54c422929121b61d1",
|
|
23
|
+
"tests/common/mod.rs": "2a2d588be1348f788d50f3eed2f8359dea88befa71762d0f1434a31f0c66ce73",
|
|
24
|
+
"tests/condense.rs": "00ea2b943597ef60bff875c64af5f98b25237c284e1c749b154d42de893d1a87",
|
|
25
|
+
"tests/cross_project_move.rs": "38ee42e866e25710482d40bc652abe17220e4ce388d2d33e3c5bf74e254d9b78",
|
|
26
|
+
"tests/download.rs": "d267215f8f58b118e414fe06deab35525e39652c2dbd0c3a222b78c24c4a26cc",
|
|
27
|
+
"tests/engine.rs": "ca85df1b5f2e34db0f743b01ddda61ca3ef97ff043acf84994b329017614bfcd",
|
|
28
|
+
"tests/mentions.rs": "686898b17aa49bc68d3f6d625195a6acf34219e22079c962611a59fe6707ecbc",
|
|
29
|
+
"tests/push.rs": "4144dd33ebec99e55c8769d7259017a75da4e4424be02d8024c44926b4eedca2",
|
|
30
|
+
"tests/resumable_upload.rs": "f8f64f21cac169ef96d3349b0676a4bbccacad5e55bd049b3b6cf66e7afda30f",
|
|
31
|
+
"tests/skills.rs": "1e1e18fcddfad5bb6ebfd2fbacb5feb66157114e3dc1939bd8aedcdff6a70b98",
|
|
32
|
+
"tests/sso.rs": "f95e566a55821a8d56b6e6c90cdf8f0f0cd245244f10261877f0340b6841be52"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DriveController } from "./core.js";
|
|
3
|
+
import { type DriveLocale } from "./locales.js";
|
|
4
|
+
export declare function DriveBadge({ controller, path, isDirectory, locale }: {
|
|
5
|
+
controller: DriveController;
|
|
6
|
+
path: string;
|
|
7
|
+
isDirectory?: boolean;
|
|
8
|
+
locale?: DriveLocale;
|
|
9
|
+
}): ReactNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as e,Fragment as a,jsxs as r}from"react/jsx-runtime";import{aggregateDirBadge as g,badgeFor as v}from"./badge.js";import{driveTranslator as M}from"./locales.js";import{useDrive as f}from"./ui.js";var u={synced:e("path",{d:"m20 6-11 11-5-5"}),pending:r(a,{children:[e("circle",{cx:"12",cy:"12",r:"9"}),e("circle",{cx:"12",cy:"12",r:"2"})]}),uploading:e("path",{d:"m8 12 4-4 4 4M12 8v12M5 17a5 5 0 0 1-1-10 7 7 0 0 1 13-1 5 5 0 0 1 3 9"}),downloading:e("path",{d:"M12 2a10 10 0 1 1-10 10"}),notDownloaded:e("path",{d:"m8 16 4 4 4-4M12 8v12M5 17a5 5 0 0 1-1-10 7 7 0 0 1 13-1 5 5 0 0 1 3 9"}),cloudOnly:e("path",{d:"M6 18a5 5 0 0 1-1-10 7 7 0 0 1 13-2 6 6 0 0 1 0 12Z"}),conflict:r(a,{children:[e("path",{d:"m12 3 10 18H2Z"}),e("path",{d:"M12 9v4M12 17v.01"})]}),error:r(a,{children:[e("circle",{cx:"12",cy:"12",r:"9"}),e("path",{d:"m8 8 8 8M8 16l8-8"})]}),ignored:e(a,{children:e("path",{d:"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12ZM3 3l18 18"})}),skippedTooLarge:r(a,{children:[e("circle",{cx:"12",cy:"12",r:"9"}),e("path",{d:"m5 19 14-14"})]}),forbidden:r(a,{children:[e("rect",{x:"5",y:"10",width:"14",height:"12",rx:"2"}),e("path",{d:"M8 10V6a4 4 0 0 1 8 0v4"})]}),readOnly:r(a,{children:[e("path",{d:"M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12V7Z"}),e("rect",{x:"9",y:"13",width:"8",height:"7",rx:"1"}),e("path",{d:"M11 13v-2a2 2 0 0 1 4 0v2"})]}),collab:r(a,{children:[e("circle",{cx:"9",cy:"7",r:"3"}),e("path",{d:"M3 21v-3a6 6 0 0 1 12 0v3M17 4a3 3 0 0 1 0 6M21 21v-3a5 5 0 0 0-4-5"})]})};function k({controller:o,path:s,isDirectory:h=!1,locale:p}){let{snapshot:m}=f(o),d=s.replaceAll("\\","/").replace(/\/$/,""),i=m?.files??[],c=i.find((t)=>t.localPath.replaceAll("\\","/")===d.replace(/\.yupcloud$/,"")),l=h?g(d,new Map(i.map((t)=>[t.localPath.replaceAll("\\","/"),t.state]))):c?v(c.state):null;if(!l)return null;let n=M(p)(l.labelKey);return e("span",{className:`alwith-drive-badge ${l.className}`,title:n,"aria-label":n,"data-state":l.state,children:e("svg",{className:l.spin?"alwith-drive-spin":void 0,width:"14",height:"14",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"1.75",fill:"none",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:u[l.state]})})}export{k as DriveBadge};
|
package/dist/badge.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FileState } from "./core.js";
|
|
2
|
+
export type DriveBadgeDescriptor = {
|
|
3
|
+
state: FileState;
|
|
4
|
+
icon: "check" | "cloud-upload" | "triangle-alert" | "cloud" | "eye-off" | "circle-slash" | "circle-x" | "circle-dot" | "lock" | "file-lock" | "users" | "cloud-download" | "loader-circle";
|
|
5
|
+
className: string;
|
|
6
|
+
labelKey: `drive.state.${FileState}`;
|
|
7
|
+
spin: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function badgeFor(state: FileState): DriveBadgeDescriptor;
|
|
10
|
+
export declare function aggregateDirBadge(dirPath: string, byPath: ReadonlyMap<string, FileState>): DriveBadgeDescriptor | null;
|
package/dist/badge.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var t={synced:{icon:"check",className:"text-primary",spin:!1},pending:{icon:"circle-dot",className:"text-primary",spin:!1},uploading:{icon:"cloud-upload",className:"text-primary",spin:!0},conflict:{icon:"triangle-alert",className:"text-destructive",spin:!1},cloudOnly:{icon:"cloud",className:"text-muted-foreground",spin:!1},notDownloaded:{icon:"cloud-download",className:"text-primary",spin:!1},downloading:{icon:"loader-circle",className:"text-primary",spin:!0},ignored:{icon:"eye-off",className:"text-muted-foreground",spin:!1},skippedTooLarge:{icon:"circle-slash",className:"text-primary",spin:!1},forbidden:{icon:"lock",className:"text-muted-foreground",spin:!1},readOnly:{icon:"file-lock",className:"text-primary",spin:!1},error:{icon:"circle-x",className:"text-destructive",spin:!1},collab:{icon:"users",className:"text-primary",spin:!1}};function r(e){return{state:e,labelKey:`drive.state.${e}`,...t[e]}}function c(e,i){let n=`${e}/`,a=null;for(let[o,s]of i){if(!o.startsWith(n))continue;if(s==="conflict")return r("conflict");if(s==="error")a="error"}return a?r(a):null}export{c as aggregateDirBadge,r as badgeFor};
|
package/dist/browse.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProjectProgress, RemoteId, RootInfo } from "./core.js";
|
|
2
|
+
export type DriveGroupKey = "mine" | "shared";
|
|
3
|
+
export declare const ARCHIVE_PROJECT_KEY = "ai-session-archive";
|
|
4
|
+
export declare function isBrowsable(root: RootInfo): boolean;
|
|
5
|
+
export declare function hasLocation(root: Pick<RootInfo, "drivePath">): boolean;
|
|
6
|
+
export declare function groupOf(root: RootInfo): DriveGroupKey;
|
|
7
|
+
export declare function sharedScopeKey(root: RootInfo): "dept" | "team" | "company" | "personal";
|
|
8
|
+
export type DriveGroup = {
|
|
9
|
+
nodePath: string | null;
|
|
10
|
+
label: string | null;
|
|
11
|
+
legacy: DriveGroupKey | null;
|
|
12
|
+
items: RootInfo[];
|
|
13
|
+
};
|
|
14
|
+
export declare function containerKey(root: RootInfo): "material" | "knowledge";
|
|
15
|
+
export declare function groupsOf(roots: readonly RootInfo[]): DriveGroup[];
|
|
16
|
+
export declare function idKey(id: RemoteId): string;
|
|
17
|
+
export declare function hasLanded(root: RootInfo): boolean;
|
|
18
|
+
export type RowStatus = {
|
|
19
|
+
kind: "opening";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "queued";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "syncing";
|
|
24
|
+
done: number;
|
|
25
|
+
total: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "failed";
|
|
28
|
+
message: string | null;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "partial";
|
|
31
|
+
failed: number;
|
|
32
|
+
};
|
|
33
|
+
export declare function rowStatusOf(progress: ProjectProgress | undefined, opening: boolean): RowStatus | null;
|
package/dist/browse.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var m="ai-session-archive";function p(e){return e.kind!=="SKILL"&&e.projectKey!=="ai-session-archive"&&h(e)}function h(e){return(e.drivePath??"").trim()!==""}function P(e){if(e.isMine===!0)return"mine";if(e.isMine===!1)return"shared";return e.scope==="PERSONAL"?"mine":"shared"}function O(e){if(e.scope==="PERSONAL")return"personal";if(e.scope==="TEAM")return"team";if(e.scope==="DEPT")return"dept";return"company"}var o={PERSONAL:0,DEPT:1,COMPANY:2};function s(e){return(e.container??e.kind)==="KNOWLEDGE"?"knowledge":"material"}function k(e){let t=new Map,a=new Map;for(let n of e){if(!p(n))continue;if(n.nodePath===null){let r=P(n),u=a.get(r)??{nodePath:null,label:null,legacy:r,items:[]};u.items.push(n),a.set(r,u);continue}let i=t.get(n.nodePath)??{nodePath:n.nodePath,label:E(n.nodePath),legacy:null,items:[]};i.items.push(n),t.set(n.nodePath,i)}for(let n of t.values())n.items.sort((i,r)=>Number(s(i)==="knowledge")-Number(s(r)==="knowledge"));let d=[...t.values()].toSorted((n,i)=>{let r=l(n)-l(i);return r!==0?r:(n.label??"").localeCompare(i.label??"","zh-Hans-CN")}),f=["mine","shared"].flatMap((n)=>a.get(n)??[]);return[...d,...f]}function l(e){let t=e.items[0]?.node;if(t!==null&&t!==void 0&&t in o)return o[t];if(e.nodePath==="公司")return o.COMPANY;if(e.nodePath==="我的空间")return o.PERSONAL;return o.DEPT}function E(e){return e.startsWith(c)?e.slice(c.length):e}var c="部门/";function g(e){return String(e)}function y(e){return e.cursor!==null}function N(e,t){if(e!==void 0)switch(e.phase){case"queued":return{kind:"queued"};case"syncing":return{kind:"syncing",done:e.done,total:e.total};case"failed":return{kind:"failed",message:e.message??null};case"done":if(e.failed>0)return{kind:"partial",failed:e.failed};break}return t?{kind:"opening"}:null}export{m as ARCHIVE_PROJECT_KEY,s as containerKey,P as groupOf,k as groupsOf,y as hasLanded,h as hasLocation,g as idKey,p as isBrowsable,N as rowStatusOf,O as sharedScopeKey};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { DriveController, Job, RemoteId } from "../core.js";
|
|
3
|
+
import { type DriveLocale } from "../locales.js";
|
|
4
|
+
import type { DriveControls } from "../react.js";
|
|
5
|
+
import type { CondenseScope } from "./condense.js";
|
|
6
|
+
export declare const CONDENSE_SILENCE_MS: number;
|
|
7
|
+
export interface CondenseSnoozeHost {
|
|
8
|
+
readSnooze: (projectId: RemoteId) => Promise<number | null>;
|
|
9
|
+
writeSnooze: (projectId: RemoteId, until: number) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function pendingCondenseProposal(controller: Pick<DriveController, "request">, host: CondenseSnoozeHost, projectId: RemoteId, now?: number): Promise<Job | null>;
|
|
12
|
+
export declare function dismissCondenseProposal(controller: Pick<DriveController, "request">, host: CondenseSnoozeHost, projectId: RemoteId, jobId: RemoteId, now?: number): Promise<void>;
|
|
13
|
+
export interface ChatCondenseEntryProps {
|
|
14
|
+
cwd: string | null | undefined;
|
|
15
|
+
controller: DriveController;
|
|
16
|
+
controls: DriveControls;
|
|
17
|
+
locale?: DriveLocale;
|
|
18
|
+
host: CondenseSnoozeHost;
|
|
19
|
+
onCondense: (scope: CondenseScope, initialJob?: Job) => void;
|
|
20
|
+
onBackgroundError: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function ChatCondenseEntry({ cwd, controller, controls: { Button }, locale, host, onCondense, onBackgroundError }: ChatCondenseEntryProps): ReactNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as t,jsxs as P}from"react/jsx-runtime";import{SparklesIcon as O}from"lucide-react";import{useCallback as R,useEffect as J,useRef as x,useState as b,useSyncExternalStore as M}from"react";import{driveTranslator as A}from"../locales.js";import{resolveCondenseScope as T}from"./file-actions.js";var F=2592000000;async function G(i,e,r,h=Date.now()){if((await e.readSnooze(r)??0)>h)return null;let s=await i.request({type:"proposedCondense",projectId:r});if(s.type!=="jobs")throw Error("Invalid proposed condense response");return s.data.find((c)=>c.status==="PROPOSED"&&(c.type??"condense")==="condense")??null}async function H(i,e,r,h,s=Date.now()){let o=(await Promise.allSettled([i.request({type:"cancelJob",jobId:h}).then((l)=>{if(l.type!=="done")throw Error("Invalid proposal cancellation response")}),e.writeSnooze(r,s+F)])).flatMap((l)=>l.status==="rejected"?[l.reason]:[]);if(o.length)throw AggregateError(o,o.map(String).join("; "))}function X({cwd:i,controller:e,controls:{Button:r},locale:h="en",host:s,onCondense:c,onBackgroundError:o}){let l=M(e.subscribe,e.getSnapshot,e.getSnapshot),u=i?T(i,l.snapshot?.roots??[]):null,n=u?.projectId??null,q=u?.projectName??"",d=A(h),[j,m]=b(null),p=j&&String(j.projectId)===String(n)?j.job:null,[E,k]=b(!1),[z,C]=b(null),[D,N]=b(null),v=x(0),w=x(n);w.current=n;let S=x(!1),a=x(!1),y=R(()=>{if(S.current)return;let g=++v.current;if(n===null){m(null);return}G(e,s,n).then((f)=>{if(a.current&&g===v.current)m(f?{projectId:n,job:f}:null)}).catch(o)},[e,s,n,o]);J(()=>{let g=v;return a.current=!0,m(null),C(null),N(null),y(),window.addEventListener("focus",y),()=>{a.current=!1,g.current++,window.removeEventListener("focus",y)}},[y]);let _=async(g)=>{if(!p||!u||n===null||S.current)return;let f=n;S.current=!0,v.current++,k(!0),C(null),N(null);try{if(g){if((await e.request({type:"approveJob",jobId:p.id})).type!=="done")throw Error("Invalid proposal approval response");let L=await e.request({type:"job",jobId:p.id});if(L.type!=="job")throw Error("Invalid approved condense job response");if(a.current&&String(w.current)===String(f))m(null),c({...u,scopePath:null},L.data)}else if(m(null),await H(e,s,n,p.id),a.current&&String(w.current)===String(f))N(d("condense.propose.dismissed",{name:q}))}catch(I){if(a.current&&String(w.current)===String(f))C(String(I));else o(I)}finally{if(S.current=!1,a.current)k(!1)}};if(!u)return null;return P("div",{className:"alwith-drive-chat-condense",children:[p?P("div",{role:"status",title:p.message??void 0,className:"text-muted-foreground flex flex-wrap items-center gap-3 text-xs",children:[t(O,{className:"size-3.5 shrink-0","aria-hidden":"true"}),t("span",{children:d("condense.propose.title",{name:q})}),t(r,{variant:"ghost",disabled:E,onClick:()=>void _(!0),children:d("condense.propose.action")}),t(r,{variant:"ghost",disabled:E,onClick:()=>void _(!1),children:d("condense.propose.dismiss")})]}):t("span",{title:d("condense.action"),children:P(r,{variant:"ghost",className:"text-muted-foreground hover:text-foreground size-6 shrink-0 p-0",disabled:E,onClick:()=>c({...u,scopePath:null}),children:[t(O,{className:"size-3.5","aria-hidden":"true"}),t("span",{className:"sr-only",children:d("condense.action")})]})}),z&&t("p",{role:"alert",children:z}),D&&t("p",{role:"status",children:D})]})}export{F as CONDENSE_SILENCE_MS,X as ChatCondenseEntry,H as dismissCondenseProposal,G as pendingCondenseProposal};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Ctx } from "@milkdown/ctx";
|
|
2
|
+
import type { Awareness } from "y-protocols/awareness";
|
|
3
|
+
import { WebsocketProvider } from "y-websocket";
|
|
4
|
+
import * as Y from "yjs";
|
|
5
|
+
import type { CollabSession } from "../contract.js";
|
|
6
|
+
export declare const AI_ORIGIN: {
|
|
7
|
+
readonly author: "AI";
|
|
8
|
+
};
|
|
9
|
+
export declare const AUTHOR_FIELD = "author";
|
|
10
|
+
export declare const AI_AUTHOR = "AI";
|
|
11
|
+
export declare function splitFrontmatter(markdown: string): {
|
|
12
|
+
frontmatter: string;
|
|
13
|
+
body: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const FRAGMENT_NAME = "prosemirror";
|
|
16
|
+
export type CollabUser = {
|
|
17
|
+
name: string;
|
|
18
|
+
color: string;
|
|
19
|
+
};
|
|
20
|
+
export type Collaborator = {
|
|
21
|
+
clientId: number;
|
|
22
|
+
user: CollabUser;
|
|
23
|
+
};
|
|
24
|
+
export declare function colorFor(name: string): string;
|
|
25
|
+
export declare function listCollaborators(awareness: Awareness): Collaborator[];
|
|
26
|
+
export type CollabConnection = {
|
|
27
|
+
doc: Y.Doc;
|
|
28
|
+
provider: WebsocketProvider;
|
|
29
|
+
awareness: Awareness;
|
|
30
|
+
ready: Promise<boolean>;
|
|
31
|
+
destroy: () => void;
|
|
32
|
+
};
|
|
33
|
+
export declare function connectCollab(session: CollabSession, user: CollabUser): CollabConnection;
|
|
34
|
+
export declare function applyDiskMarkdown(ctx: Ctx, connection: Pick<CollabConnection, "doc" | "awareness">, body: string, userName: string): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{editorViewCtx as m,parserCtx as y}from"@milkdown/core";import{updateYFragment as g,ySyncPluginKey as h}from"y-prosemirror";import{WebsocketProvider as x}from"y-websocket";import*as p from"yjs";var S={author:"AI"},d="author",b="AI";function L(t){let e=/^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/.exec(t);if(!e)return{frontmatter:"",body:t};return{frontmatter:e[0],body:t.slice(e[0].length)}}var F="prosemirror",u=["#f97316","#10b981","#3b82f6","#a855f7","#ec4899","#14b8a6","#eab308","#ef4444"];function k(t){let e=0;for(let o of t)e=e*31+o.charCodeAt(0)>>>0;return u[e%u.length]}function E(t){let e=[];for(let[o,r]of t.getStates()){if(o===t.clientID)continue;let n=r.user;if(!n)continue;e.push({clientId:o,user:n})}return e.toSorted((o,r)=>o.clientId-r.clientId)}function T(t,e){let o=new p.Doc,r=new x(t.serverUrl,t.room,o,{params:{token:t.token},maxBackoffTime:5000}),n=r.awareness;n.setLocalStateField("user",e),n.setLocalStateField(d,e.name);let f=()=>{},i=new Promise((l)=>{let c=!1,s=(a)=>{if(c)return;c=!0,l(a)};f=s,r.on("sync",(a)=>{if(a)s(!0)}),r.on("connection-error",()=>s(!1)),r.on("connection-close",()=>s(!1))});return{doc:o,provider:r,awareness:n,ready:i,destroy:()=>{f(!1),n.setLocalState(null),r.destroy(),o.destroy()}}}function C(t,e,o,r){let n=t.get(m),i=h.getState(n.state)?.binding;if(!i)return!1;let l=t.get(y)(o);if(!l)throw Error("Failed to parse disk Markdown");let{doc:c,awareness:s}=e,a=c.getXmlFragment(F);s.setLocalStateField(d,b);try{c.transact(()=>{g(c,a,l,i)},S)}finally{s.setLocalStateField(d,r)}return!0}export{b as AI_AUTHOR,S as AI_ORIGIN,d as AUTHOR_FIELD,F as FRAGMENT_NAME,C as applyDiskMarkdown,k as colorFor,T as connectCollab,E as listCollaborators,L as splitFrontmatter};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type QuoteRange = {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
};
|
|
5
|
+
export type TextSegment = {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
origin: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function locateQuote(text: string, quote: string, quoteOffset: number | null): QuoteRange | null;
|
|
11
|
+
export declare function originAt(segments: TextSegment[], index: number): number | null;
|
|
12
|
+
export type CommentAnchor = {
|
|
13
|
+
threadId: string;
|
|
14
|
+
index: number;
|
|
15
|
+
quote: string;
|
|
16
|
+
quoteOffset: number | null;
|
|
17
|
+
};
|
|
18
|
+
export declare function collectAnchors(comments: {
|
|
19
|
+
id: string;
|
|
20
|
+
quote?: string | null;
|
|
21
|
+
quoteOffset?: number | null;
|
|
22
|
+
deleted?: boolean;
|
|
23
|
+
}[]): CommentAnchor[];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function f(l,t){let e=[],n=l.indexOf(t);while(n>=0)e.push(n),n=l.indexOf(t,n+1);return e}function d(l,t){if(l.length===0)return null;if(t===null)return l[0];let e=l[0],n=Math.abs(e-t);for(let r of l.slice(1)){let i=Math.abs(r-t);if(i<n)e=r,n=i}return e}function h(l){let t="",e=[];for(let n=0;n<l.length;n++){let r=l[n];if(r===`
|
|
2
|
+
`||r==="\r")continue;t+=r,e.push(n)}return{stripped:t,originalIndex:e}}function g(l,t,e){if(t.length===0)return null;let n=d(f(l,t),e);if(n!==null)return{start:n,end:n+t.length};let r=t.replaceAll(/[\r\n]+/g,"");if(r.length===0)return null;let{stripped:i,originalIndex:o}=h(l),c=null;if(e!==null){let s=o.findIndex((a)=>a>=e);c=s===-1?i.length:s}let u=d(f(i,r),c);if(u===null)return null;return{start:o[u],end:o[u+r.length-1]+1}}function p(l,t){for(let n of l)if(t>=n.start&&t<n.end)return n.origin+(t-n.start);let e=l.at(-1);if(t===e?.end)return e.origin+(e.end-e.start);return null}function x(l){let t=[];for(let e of l){if(e.deleted===!0)continue;let n=e.quote;if(n===void 0||n===null||n.length===0)continue;t.push({threadId:e.id,index:t.length+1,quote:n,quoteOffset:e.quoteOffset??null})}return t}export{x as collectAnchors,g as locateQuote,p as originAt};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Node as ProseNode } from "@milkdown/prose/model";
|
|
2
|
+
import { Plugin, PluginKey } from "@milkdown/prose/state";
|
|
3
|
+
import { DecorationSet, type EditorView } from "@milkdown/prose/view";
|
|
4
|
+
import { type CommentAnchor, type TextSegment } from "./comment-anchors.js";
|
|
5
|
+
export declare const commentAnchorsKey: PluginKey<CommentAnchorsState>;
|
|
6
|
+
export type CommentAnchorsState = {
|
|
7
|
+
anchors: CommentAnchor[];
|
|
8
|
+
decorations: DecorationSet;
|
|
9
|
+
anchored: string[];
|
|
10
|
+
};
|
|
11
|
+
export type CommentAnchorBridge = {
|
|
12
|
+
view: EditorView | null;
|
|
13
|
+
onMarkerClick: (threadId: string) => void;
|
|
14
|
+
onAnchoredChange: (threadIds: string[]) => void;
|
|
15
|
+
};
|
|
16
|
+
export declare function createCommentAnchorBridge(): CommentAnchorBridge;
|
|
17
|
+
export declare function flattenDocument(doc: ProseNode): {
|
|
18
|
+
text: string;
|
|
19
|
+
segments: TextSegment[];
|
|
20
|
+
};
|
|
21
|
+
export declare function commentAnchorsPlugin(bridge: CommentAnchorBridge): Plugin<CommentAnchorsState>;
|
|
22
|
+
export declare function pushAnchors(view: EditorView, anchors: CommentAnchor[]): void;
|
|
23
|
+
export declare function charIndexAt(segments: TextSegment[], origin: number): number | null;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{Plugin as x,PluginKey as y}from"@milkdown/prose/state";import{Decoration as f,DecorationSet as h}from"@milkdown/prose/view";import{locateQuote as k,originAt as g}from"./comment-anchors.js";var u=new y("yupCommentAnchors");function D(){return{view:null,onMarkerClick:()=>{},onAnchoredChange:()=>{}}}function A(n){let e=[],t="";return n.descendants((r,s)=>{if(r.isText){let o=r.text??"";if(o.length===0)return!1;return e.push({start:t.length,end:t.length+o.length,origin:s}),t+=o,!1}if(r.isBlock&&t.length>0&&!t.endsWith(`
|
|
2
|
+
`))e.push({start:t.length,end:t.length+1,origin:s}),t+=`
|
|
3
|
+
`;return!0}),{text:t,segments:e}}function C(n,e,t){if(e.length===0)return{decorations:h.empty,anchored:[]};let{text:r,segments:s}=A(n),o=[],i=[];for(let c of e){let l=k(r,c.quote,c.quoteOffset);if(l===null)continue;let m=g(s,l.start),d=g(s,l.end);if(m===null||d===null||d<=m)continue;o.push(f.inline(m,d,{class:"yup-comment-highlight","data-thread-id":c.threadId})),o.push(f.widget(d,()=>{let a=document.createElement("span");return a.className="yup-comment-marker",a.dataset.threadId=c.threadId,a.textContent=String(c.index),a.addEventListener("mousedown",(p)=>p.preventDefault()),a.addEventListener("click",()=>t.onMarkerClick(c.threadId)),a})),i.push(c.threadId)}return{decorations:h.create(n,o),anchored:i}}function S(n){let e="";return new x({key:u,state:{init:()=>({anchors:[],decorations:h.empty,anchored:[]}),apply(t,r,s,o){let i=t.getMeta(u);if(i===void 0&&!t.docChanged)return r;let c=i??r.anchors;return{anchors:c,...C(o.doc,c,n)}}},props:{decorations:(t)=>u.getState(t)?.decorations??h.empty},view:(t)=>(n.view=t,{update(r){let s=u.getState(r.state)?.anchored??[],o=s.join(",");if(o===e)return;e=o,n.onAnchoredChange(s)},destroy(){n.view=null}})})}function M(n,e){n.dispatch(n.state.tr.setMeta(u,e))}function E(n,e){for(let t of n){let r=t.end-t.start;if(e>=t.origin&&e<t.origin+r)return t.start+(e-t.origin)}return null}export{E as charIndexAt,u as commentAnchorsKey,S as commentAnchorsPlugin,D as createCommentAnchorBridge,A as flattenDocument,M as pushAnchors};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CommentContext, FileComment, RemoteId } from "../contract.js";
|
|
3
|
+
import type { CommentAnchor } from "./comment-anchors.js";
|
|
4
|
+
export type CommentSelection = {
|
|
5
|
+
quote: string;
|
|
6
|
+
quoteOffset: number | null;
|
|
7
|
+
rect: {
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type CommentDrawerProps = {
|
|
13
|
+
context: CommentContext;
|
|
14
|
+
comments: FileComment[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
create: (request: {
|
|
17
|
+
body: string;
|
|
18
|
+
parentId: RemoteId | null;
|
|
19
|
+
quote: string | null;
|
|
20
|
+
quoteOffset: number | null;
|
|
21
|
+
suggestedReplacement: string | null;
|
|
22
|
+
mentionedUserIds: RemoteId[] | null;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
update: (commentId: RemoteId, body: string) => Promise<void>;
|
|
25
|
+
remove: (commentId: RemoteId) => Promise<void>;
|
|
26
|
+
apply: (commentId: RemoteId) => Promise<void>;
|
|
27
|
+
selection: CommentSelection | null;
|
|
28
|
+
onSelectionUsed: () => void;
|
|
29
|
+
onFocusQuote: (threadId: string) => void;
|
|
30
|
+
focusRequest: {
|
|
31
|
+
threadId: string;
|
|
32
|
+
nonce: number;
|
|
33
|
+
} | null;
|
|
34
|
+
anchoredThreadIds: Set<string>;
|
|
35
|
+
anchors: CommentAnchor[];
|
|
36
|
+
};
|
|
37
|
+
export declare function CommentDrawer({ context, comments, loading, create, update, remove, apply, selection, onSelectionUsed, onFocusQuote, focusRequest, anchoredThreadIds, anchors }: CommentDrawerProps): import("react").JSX.Element;
|
|
38
|
+
export declare function SelectionCommentButton({ rect, onClick }: {
|
|
39
|
+
rect: {
|
|
40
|
+
top: number;
|
|
41
|
+
left: number;
|
|
42
|
+
};
|
|
43
|
+
onClick: () => void;
|
|
44
|
+
}): ReactNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as t,jsxs as o,Fragment as te}from"react/jsx-runtime";import{CheckIcon as ne,ChevronLeftIcon as re,ChevronRightIcon as se,Loader2Icon as ae,MessageSquareIcon as le,QuoteIcon as oe,WifiOffIcon as ie}from"lucide-react";import{useCallback as V,useEffect as _,useLayoutEffect as de,useMemo as ce,useRef as W,useState as v}from"react";import{createPortal as ue}from"react-dom";import{Button as R,Checkbox as me,Textarea as E,useTranslation as q}from"./host.js";import{MentionedBody as pe,MentionPicker as fe}from"./mention-picker.js";import{activeMentionQuery as he,applyMention as ge,collectMentionedIds as Z}from"./comment-mentions.js";import{cn as G,useContentHost as xe}from"./host.js";var k=(e)=>String(e);function be(){let{t:e,i18n:n}=q("common");return V((s)=>{let p=/[Z+]|[+-]\d\d:\d\d$/.test(s)?s:`${s}Z`,a=new Date(p).getTime();if(Number.isNaN(a))return s;let l=Math.floor((Date.now()-a)/60000);if(l<1)return e("drive.comments.justNow");if(l<60)return e("drive.comments.minutesAgo",{count:l});let i=Math.floor(l/60);if(i<24)return e("drive.comments.hoursAgo",{count:i});let r=Math.floor(i/24);if(r<7)return e("drive.comments.daysAgo",{count:r});return new Date(a).toLocaleDateString(n.language)},[e,n.language])}var Q=["#f97316","#10b981","#3b82f6","#a855f7","#ec4899","#14b8a6","#eab308","#ef4444"];function ve(e){let n=0;for(let s of e)n=n*31+s.charCodeAt(0)>>>0;return Q[n%Q.length]}function ye(e){return e.filter((n)=>n.deleted!==!0).length}function Fe({context:e,comments:n,loading:s,create:p,update:a,remove:l,apply:i,selection:r,onSelectionUsed:x,onFocusQuote:y,focusRequest:m,anchoredThreadIds:g,anchors:f}){let{t:c}=q("common"),[w,b]=v(!1),[S,C]=v(null),[A,F]=v(null),[M,N]=v(!1),[T,H]=v(null),[B,j]=v(!1),D=W(new Map),{pushConnected:Y,notify:z}=xe(),ee=ce(()=>new Map(f.map((u)=>[u.threadId,u.index])),[f]);_(()=>{if(m===null)return;let{threadId:u}=m;b(!0);let d=D.current.get(u);if(d)d.scrollIntoView({behavior:"smooth",block:"nearest"});H(u);let h=setTimeout(()=>H((I)=>I===u?null:I),1200);return()=>clearTimeout(h)},[m]),_(()=>{if(r!==null)b(!0)},[r]);let O=V(async(u)=>{N(!0);try{return await p(u),C(null),x(),!0}catch(d){return z.error(c("drive.comments.submitFailed"),{description:String(d)}),!1}finally{N(!1)}},[p,x,c,z]),P=ye(n);if(!w)return o("button",{type:"button","aria-label":c("drive.comments.expand"),title:c("drive.comments.title"),"data-testid":"comment-rail",onClick:()=>b(!0),className:"bg-background/95 hover:bg-accent absolute top-0 right-0 bottom-0 z-10 flex w-8 shrink-0 flex-col items-center gap-2 border-l pt-2 backdrop-blur",children:[t(re,{className:"text-muted-foreground size-3.5"}),t(le,{className:"text-muted-foreground size-3.5"}),P>0&&t("span",{className:"bg-primary text-primary-foreground flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-semibold",children:P})]});return o("div",{"data-testid":"comment-drawer",className:"bg-background/98 absolute top-0 right-0 bottom-0 z-10 flex w-80 flex-col border-l shadow-[-4px_0_12px_rgba(0,0,0,0.08)] backdrop-blur",children:[o("div",{className:"flex h-8 shrink-0 items-center gap-1.5 border-b px-2",children:[t("button",{type:"button","aria-label":c("drive.comments.collapse"),className:"text-muted-foreground hover:text-foreground",onClick:()=>b(!1),children:t(se,{className:"size-3.5"})}),t("span",{className:"text-[11px] font-semibold tracking-wide uppercase",children:c("drive.comments.title")}),t("span",{className:"text-muted-foreground text-[11px]",children:P}),t("div",{className:"flex-1"}),!Y&&o("span",{className:"text-muted-foreground flex items-center gap-1 text-[10px]",title:c("drive.comments.offlineHint"),children:[t(ie,{className:"size-3"}),c("drive.comments.offline")]})]}),o("div",{className:"min-h-0 flex-1 overflow-y-auto px-2 py-2",children:[r===null&&!B&&t(R,{variant:"outline",size:"sm",className:"mb-2 h-7 w-full text-[11px]",onClick:()=>j(!0),children:c("drive.comments.newForFile")}),r===null&&B&&t(K,{quote:null,busy:M,onCancel:()=>j(!1),onSubmit:(u,d,h)=>{O({body:u,parentId:null,quote:null,quoteOffset:null,suggestedReplacement:null,mentionedUserIds:h}).then((I)=>{if(I)j(!1)})}}),r!==null&&t(K,{quote:r.quote,busy:M,onCancel:x,onSubmit:(u,d,h)=>O({body:u,parentId:null,quote:r.quote,quoteOffset:r.quoteOffset,suggestedReplacement:d,mentionedUserIds:h})},`${r.quote}:${r.quoteOffset}`),s&&o("div",{className:"text-muted-foreground flex items-center gap-2 py-6 text-xs",children:[t(ae,{className:"size-3.5 animate-spin"}),c("drive.comments.loading")]}),!s&&n.length===0&&r===null&&!B&&t("div",{className:"text-muted-foreground py-6 text-center text-xs",children:c("drive.comments.empty")}),n.map((u)=>t(Ne,{comment:u,context:e,anchorNumber:g.has(k(u.id))?ee.get(k(u.id))??null:null,flashing:T===k(u.id),replying:S===k(u.id),editing:A,busy:M,registerRef:(d,h)=>{if(h)D.current.set(d,h);else D.current.delete(d)},onFocusQuote:y,onStartReply:(d)=>C((h)=>h===d?null:d),onStartEdit:F,onSubmitReply:(d)=>O({body:d,parentId:u.id,quote:null,quoteOffset:null,suggestedReplacement:null,mentionedUserIds:null}),onSubmitEdit:async(d,h)=>{N(!0);try{await a(d,h),F(null)}catch(I){z.error(c("drive.comments.submitFailed"),{description:String(I)})}finally{N(!1)}},onDelete:async(d)=>{N(!0);try{await l(d)}catch(h){z.error(c("drive.comments.deleteFailed"),{description:String(h)})}finally{N(!1)}},onApply:async(d)=>{N(!0);try{await i(d),z.success(c("drive.comments.applied"))}catch(h){z.error(c("drive.comments.applyFailed"),{description:String(h)})}finally{N(!1)}}},k(u.id)))]}),r===null&&t("div",{className:"text-muted-foreground shrink-0 border-t px-2 py-1.5 text-[10px] leading-4",children:c("drive.comments.selectHint")})]})}function L(e){let n=W(null);return de(()=>{let s=n.current;if(!s)return;s.style.height="auto",s.style.height=`${s.scrollHeight}px`},[e]),n}function J({value:e,onChange:n,onPicked:s,placeholder:p,rows:a=2,autoFocus:l}){let i=L(e),[r,x]=v(null),y=(m,g)=>x(he(m,g));return o("div",{className:"relative",children:[t(E,{ref:i,value:e,onChange:(m)=>{n(m.target.value),y(m.target.value,m.target.selectionStart??m.target.value.length)},onKeyUp:(m)=>{let g=m.currentTarget;y(g.value,g.selectionStart??g.value.length)},onBlur:()=>x(null),placeholder:p,rows:a,className:"min-h-14 overflow-hidden text-xs",autoFocus:l}),t(fe,{mention:r,onPick:(m)=>{if(r===null)return;let g=ge(e,r,m);n(g.body),s(m),x(null),requestAnimationFrame(()=>{i.current?.focus(),i.current?.setSelectionRange(g.caret,g.caret)})}})]})}function K({quote:e,busy:n,onCancel:s,onSubmit:p}){let{t:a}=q("common"),[l,i]=v(""),[r,x]=v(!1),[y,m]=v(""),g=L(y),[f,c]=v([]),w=l.trim().length>0&&(!r||y.trim().length>0);return o("div",{className:"border-primary/60 mb-2 rounded-md border p-2 shadow-sm","data-testid":"comment-compose",children:[e!==null&&o(te,{children:[t(X,{text:e}),o("label",{className:"text-muted-foreground my-1.5 flex cursor-pointer items-center gap-1.5 text-[11px]",children:[t(me,{checked:r,onCheckedChange:(b)=>x(b===!0)}),a("drive.comments.suggestEdit")]})]}),r&&t(E,{ref:g,value:y,onChange:(b)=>m(b.target.value),placeholder:a("drive.comments.suggestPlaceholder"),rows:2,className:"mb-1.5 min-h-14 overflow-hidden text-xs"}),t(J,{value:l,onChange:i,onPicked:(b)=>c((S)=>[...S,b]),placeholder:a("drive.comments.placeholder"),autoFocus:!0}),o("div",{className:"mt-1.5 flex justify-end gap-1.5",children:[t(R,{variant:"ghost",size:"sm",className:"h-6 px-2 text-[11px]",onClick:s,children:a("drive.comments.cancel")}),t(R,{size:"sm",className:"h-6 px-2 text-[11px]",disabled:!w||n,onClick:()=>p(l.trim(),r?y.trim():null,Z(l,f)),children:a("drive.comments.send")})]})]})}function X({text:e,onClick:n}){if(!n)return t("blockquote",{className:"block rounded-e border-s-[3px] border-amber-600 bg-amber-50 px-2 py-1 text-[11px] leading-snug break-words whitespace-pre-wrap text-amber-800 dark:bg-amber-950/40 dark:text-amber-300",children:e});return t("button",{type:"button",className:G("block rounded-e border-s-[3px] border-amber-600 bg-amber-50 px-2 py-1 text-[11px] leading-snug break-words whitespace-pre-wrap text-amber-800 dark:bg-amber-950/40 dark:text-amber-300","w-full cursor-pointer text-start"),onClick:n,children:e})}function Ne(e){let{comment:n,flashing:s,replying:p,registerRef:a}=e,l=k(n.id);return o("div",{ref:(i)=>a(l,i),"data-testid":"comment-thread",className:G("mb-2 rounded-md border px-2 py-1.5 transition-colors",s&&"bg-primary/10"),children:[t(U,{...e,comment:n,depth:0}),(n.replies??[]).map((i)=>t("div",{className:"bg-muted/40 ms-3 mt-1.5 rounded px-2 py-1",children:t(U,{...e,comment:i,depth:1})},k(i.id))),p&&t(ke,{busy:e.busy,onCancel:()=>e.onStartReply(l),onSubmit:(i,r)=>e.onSubmitReply(i,r)})]})}function U({comment:e,context:n,anchorNumber:s,editing:p,busy:a,depth:l,onFocusQuote:i,onStartReply:r,onStartEdit:x,onSubmitEdit:y,onDelete:m,onApply:g}){let{t:f}=q("common"),c=be(),w=k(e.id),b=e.authorName??"?",S=k(e.authorId)===k(n.currentUserId),C=p===w,[A,F]=v(e.body??""),M=L(A);if(_(()=>{if(C)F(e.body??"")},[C,e.body]),e.deleted===!0)return t("div",{className:"text-muted-foreground py-0.5 text-[11px] italic",children:f("drive.comments.deletedPlaceholder")});let N=n.isOwner&&l===0&&e.suggestedReplacement!==void 0&&e.suggestedReplacement!==null&&e.suggestedReplacement.length>0&&(e.appliedAt===void 0||e.appliedAt===null);return o("div",{className:"group/comment flex gap-2",children:[t("span",{className:"mt-0.5 flex shrink-0 items-center justify-center rounded-full font-semibold text-white",style:{backgroundColor:ve(b),width:l===0?22:18,height:l===0?22:18,fontSize:l===0?10:9},children:b.slice(0,1).toUpperCase()}),o("div",{className:"min-w-0 flex-1",children:[o("div",{className:"flex flex-wrap items-center gap-1 text-[11px]",children:[s!==null&&t("span",{className:"flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-500 px-1 text-[9px] font-semibold text-white",children:s}),t("span",{className:"font-semibold",children:b}),e.isAi===!0&&t("span",{className:"rounded bg-amber-100 px-1 text-[9px] font-semibold text-amber-800 dark:bg-amber-900 dark:text-amber-200",children:f("drive.comments.aiBadge")}),t("span",{className:"text-muted-foreground",children:c(e.createdAt)}),e.updatedAt!==void 0&&e.updatedAt!==null&&e.updatedAt!==e.createdAt&&t("span",{className:"text-muted-foreground",children:f("drive.comments.edited")})]}),e.quote!==void 0&&e.quote!==null&&e.quote.length>0&&t("div",{className:"mt-1",children:t(X,{text:e.quote,onClick:()=>i(w)})}),C?o("div",{className:"mt-1",children:[t(E,{ref:M,value:A,onChange:(T)=>F(T.target.value),rows:2,className:"min-h-14 overflow-hidden text-xs",autoFocus:!0}),o("div",{className:"mt-1 flex justify-end gap-1.5",children:[t(R,{variant:"ghost",size:"sm",className:"h-6 px-2 text-[11px]",onClick:()=>x(null),children:f("drive.comments.cancel")}),t(R,{size:"sm",className:"h-6 px-2 text-[11px]",disabled:a||A.trim().length===0,onClick:()=>y(e.id,A.trim()),children:f("drive.comments.save")})]})]}):t("p",{className:"mt-0.5 text-xs leading-snug break-words whitespace-pre-wrap",children:t(pe,{body:e.body??"",mentions:e.mentions})}),e.suggestedReplacement!==void 0&&e.suggestedReplacement!==null&&e.suggestedReplacement.length>0&&o("div",{className:"mt-1 rounded border-s-[3px] border-emerald-500 bg-emerald-500/10 px-2 py-1",children:[t("div",{className:"text-[10px] font-semibold text-emerald-700 dark:text-emerald-400",children:f("drive.comments.suggestLabel")}),t("div",{className:"text-[11px] leading-snug break-words whitespace-pre-wrap",children:e.suggestedReplacement}),e.appliedAt!==void 0&&e.appliedAt!==null&&o("div",{className:"mt-0.5 flex items-center gap-1 text-[10px] text-emerald-700 dark:text-emerald-400",children:[t(ne,{className:"size-3"}),f("drive.comments.appliedBy",{name:e.appliedBy?.name??"?"})]})]}),!C&&o("div",{className:"mt-0.5 flex gap-2 text-[10px] opacity-0 transition-opacity group-hover/comment:opacity-100 focus-within:opacity-100",children:[l===0&&t("button",{type:"button",className:"text-muted-foreground hover:text-foreground",onClick:()=>r(w),children:f("drive.comments.reply")}),S&&t("button",{type:"button",className:"text-muted-foreground hover:text-foreground",onClick:()=>x(w),children:f("drive.comments.edit")}),S&&t("button",{type:"button",className:"text-destructive hover:underline",disabled:a,onClick:()=>m(e.id),children:f("drive.comments.delete")}),N&&t("button",{type:"button",className:"font-semibold text-emerald-600 hover:underline",disabled:a,onClick:()=>g(e.id),children:f("drive.comments.applyAction")})]})]})]})}function ke({busy:e,onCancel:n,onSubmit:s}){let{t:p}=q("common"),[a,l]=v(""),[i,r]=v([]);return o("div",{className:"ms-3 mt-1.5","data-testid":"comment-reply",children:[t(J,{value:a,onChange:l,onPicked:(x)=>r((y)=>[...y,x]),placeholder:p("drive.comments.replyPlaceholder"),autoFocus:!0}),o("div",{className:"mt-1 flex justify-end gap-1.5",children:[t(R,{variant:"ghost",size:"sm",className:"h-6 px-2 text-[11px]",onClick:n,children:p("drive.comments.cancel")}),t(R,{size:"sm",className:"h-6 px-2 text-[11px]",disabled:e||a.trim().length===0,onClick:()=>s(a.trim(),Z(a,i)),children:p("drive.comments.send")})]})]})}function Me({rect:e,onClick:n}){let{t:s}=q("common");return ue(o("button",{type:"button","data-testid":"comment-selection-button",onMouseDown:(p)=>p.preventDefault(),onClick:n,className:"bg-primary text-primary-foreground fixed z-[9999] flex items-center gap-1 rounded px-2 py-1 text-[11px] shadow-md",style:{left:e.left,top:Math.max(e.top-32,8)},children:[t(oe,{className:"size-3"}),s("drive.comments.commentOnSelection")]}),document.body)}export{Fe as CommentDrawer,Me as SelectionCommentButton};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RemoteId, UserBrief } from "../contract.js";
|
|
2
|
+
export declare const MENTION_DEBOUNCE_MS = 180;
|
|
3
|
+
export type MentionQuery = {
|
|
4
|
+
start: number;
|
|
5
|
+
query: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function activeMentionQuery(body: string, caret: number): MentionQuery | null;
|
|
8
|
+
export declare function mentionNameOf(person: UserBrief): string;
|
|
9
|
+
export declare function applyMention(body: string, mention: MentionQuery, person: UserBrief): {
|
|
10
|
+
body: string;
|
|
11
|
+
caret: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function collectMentionedIds(body: string, picked: readonly UserBrief[]): RemoteId[] | null;
|
|
14
|
+
export type MentionSearch = {
|
|
15
|
+
query: (keyword: string) => void;
|
|
16
|
+
cancel: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function createDebouncedSearch<T>(fetch: (keyword: string) => Promise<T[]>, onResults: (results: T[]) => void, onError: (error: unknown) => void): MentionSearch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var d=180;function p(n,r){let c=n.slice(0,r),e=c.lastIndexOf("@");if(e===-1)return null;let t=e===0?"":c[e-1];if(t!==""&&!/\s/.test(t))return null;let i=c.slice(e+1);if(/\s/.test(i))return null;return{start:e,query:i}}function s(n){return n.name||n.employeeNo||String(n.id)}function m(n,r,c){let e=`@${s(c)} `;return{body:n.slice(0,r.start)+e+n.slice(r.start+1+r.query.length),caret:r.start+e.length}}function x(n,r){let c=[],e=new Set;for(let t of r){if(e.has(String(t.id)))continue;if(!n.includes(`@${s(t)}`))continue;e.add(String(t.id)),c.push(t.id)}return c.length===0?null:c}function h(n,r,c){let e=null,t=0,i=!1;return{query(a){if(i=!1,e!==null)clearTimeout(e);let u=a.trim();if(u===""){t+=1,r([]);return}let o=++t;e=setTimeout(()=>{n(u).catch((l)=>{if(o===t&&!i)c(l);return[]}).then((l)=>{if(o!==t||i)return;r(l)})},180)},cancel:()=>{if(i=!0,e!==null)clearTimeout(e);e=null,t+=1}}}export{d as MENTION_DEBOUNCE_MS,p as activeMentionQuery,m as applyMention,x as collectMentionedIds,h as createDebouncedSearch,s as mentionNameOf};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type CommentAnchor } from "./comment-anchors.js";
|
|
3
|
+
import { type CommentSelection } from "./comment-drawer.js";
|
|
4
|
+
import { type FileComments } from "./use-file-comments.js";
|
|
5
|
+
export type CommentAnchoring = {
|
|
6
|
+
selection: CommentSelection | null;
|
|
7
|
+
onSelectionUsed: () => void;
|
|
8
|
+
onFocusQuote: (threadId: string) => void;
|
|
9
|
+
focusRequest: {
|
|
10
|
+
threadId: string;
|
|
11
|
+
nonce: number;
|
|
12
|
+
} | null;
|
|
13
|
+
anchoredThreadIds: Set<string>;
|
|
14
|
+
};
|
|
15
|
+
export type CommentSurfaceApi = {
|
|
16
|
+
source: FileComments;
|
|
17
|
+
anchors: CommentAnchor[];
|
|
18
|
+
publish: (anchoring: CommentAnchoring | null) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function commentPathOf(path: string): string;
|
|
21
|
+
export declare function useCommentAnchoring(anchoring: CommentAnchoring | null): CommentSurfaceApi;
|
|
22
|
+
export declare function CommentSurface({ path, children }: {
|
|
23
|
+
path: string;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as q,jsxs as C}from"react/jsx-runtime";import{createContext as y,useCallback as u,useContext as I,useEffect as i,useMemo as d,useRef as R,useState as T}from"react";import{collectAnchors as v}from"./comment-anchors.js";import{CommentDrawer as A}from"./comment-drawer.js";import{useFileComments as P}from"./use-file-comments.js";var F=(t)=>t.replace(/\.yupcloud$/,"");function g(t,o){if(t===null||o===null)return t===o;if(t.selection?.quote!==o.selection?.quote||t.selection?.quoteOffset!==o.selection?.quoteOffset)return!1;if(t.focusRequest?.threadId!==o.focusRequest?.threadId||t.focusRequest?.nonce!==o.focusRequest?.nonce)return!1;if(t.anchoredThreadIds.size!==o.anchoredThreadIds.size)return!1;for(let e of t.anchoredThreadIds)if(!o.anchoredThreadIds.has(e))return!1;return!0}var m=y(null);function j(t){return F(t)}function z(t){let o=I(m)??E,{publish:e}=o;return i(()=>{e(t)}),i(()=>()=>e(null),[e]),o}var E={source:{context:null,comments:[],loading:!1,reload:async()=>{},create:async()=>{},update:async()=>{},remove:async()=>{},apply:async()=>{}},anchors:[],publish:()=>{}};function D({path:t,children:o}){let e=P(j(t)),s=R(null),[c,h]=T(null),a=u((n)=>{s.current=n,h((f)=>g(f,n)?f:n)},[]),p=u(()=>s.current?.onSelectionUsed(),[]),x=u((n)=>s.current?.onFocusQuote(n),[]),{comments:l}=e,r=d(()=>v(l.map((n)=>({...n,id:String(n.id)}))),[l]),S=d(()=>({source:e,anchors:r,publish:a}),[e,r,a]);return C(m.Provider,{value:S,children:[o,e.context!==null&&q(A,{context:e.context,comments:e.comments,loading:e.loading,create:e.create,update:e.update,remove:e.remove,apply:e.apply,selection:c?.selection??null,onSelectionUsed:p,onFocusQuote:x,focusRequest:c?.focusRequest??null,anchoredThreadIds:c?.anchoredThreadIds??O,anchors:r})]})}var O=new Set;export{D as CommentSurface,j as commentPathOf,z as useCommentAnchoring};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DriveController, Job, UnsyncedSummary } from "../core.js";
|
|
2
|
+
import type { CondenseScope } from "./condense.js";
|
|
3
|
+
type Phase = "idle" | "uploading" | "unsynced" | "starting" | "running" | "succeeded" | "failed" | "cancelled";
|
|
4
|
+
interface TaskState {
|
|
5
|
+
phase: Phase;
|
|
6
|
+
job: Job | null;
|
|
7
|
+
unsynced: UnsyncedSummary | null;
|
|
8
|
+
error: string | null;
|
|
9
|
+
stale: boolean;
|
|
10
|
+
uploadIssue: string | null;
|
|
11
|
+
cancelling: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class CondenseTask {
|
|
14
|
+
private controller;
|
|
15
|
+
private scope;
|
|
16
|
+
notify: ((outcome: "succeeded" | "failed") => void) | undefined;
|
|
17
|
+
private state;
|
|
18
|
+
private listeners;
|
|
19
|
+
private timer;
|
|
20
|
+
private unsubscribe;
|
|
21
|
+
private conflicts;
|
|
22
|
+
private fileStates;
|
|
23
|
+
private invalid;
|
|
24
|
+
private inFlight;
|
|
25
|
+
private failures;
|
|
26
|
+
private cancelSent;
|
|
27
|
+
private generation;
|
|
28
|
+
constructor(controller: DriveController, scope: CondenseScope, initialJob?: Job);
|
|
29
|
+
getSnapshot: () => TaskState;
|
|
30
|
+
subscribe: (listener: () => void) => (() => void);
|
|
31
|
+
private publish;
|
|
32
|
+
private watch;
|
|
33
|
+
private stop;
|
|
34
|
+
private accept;
|
|
35
|
+
resume: () => void;
|
|
36
|
+
private schedule;
|
|
37
|
+
private poll;
|
|
38
|
+
start: (skipUploads?: boolean) => Promise<void>;
|
|
39
|
+
private cancelRemote;
|
|
40
|
+
cancel: () => Promise<void>;
|
|
41
|
+
adopt: (job: Job) => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function condenseTask(controller: DriveController, scope: CondenseScope, initialJob?: Job): CondenseTask;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var h=new WeakMap;class l{controller;scope;notify;state;listeners=new Set;timer;unsubscribe;conflicts=new Set;fileStates=new Map;invalid=!1;inFlight=!1;failures=0;cancelSent=!1;generation;constructor(t,s,e){this.controller=t,this.scope=s,this.generation=t.getSnapshot().snapshot?.generation,this.state={phase:"idle",job:e??null,unsynced:null,error:null,uploadIssue:null,stale:!1,cancelling:!1}}getSnapshot=()=>this.state;subscribe=(t)=>(this.listeners.add(t),()=>{this.listeners.delete(t)});publish(t){let s=this.state.phase;if(this.state={...this.state,...t},s!==this.state.phase&&(this.state.phase==="succeeded"||this.state.phase==="failed")){if(this.state.phase==="succeeded"||this.listeners.size===0)this.notify?.(this.state.phase)}this.listeners.forEach((e)=>e())}watch(){if(this.unsubscribe)return;this.conflicts=new Set(this.controller.getSnapshot().snapshot?.status?.conflicts.map((t)=>`${t.localPath}:${t.at}`)),this.fileStates=new Map(this.controller.getSnapshot().snapshot?.files.map((t)=>[t.localPath,t.state])),this.unsubscribe=this.controller.subscribe(()=>{let t=this.controller.getSnapshot().snapshot;if(this.state.phase==="uploading"&&t){for(let s of t.status?.conflicts??[]){let e=`${s.localPath}:${s.at}`;if(String(s.projectId)===String(this.scope.projectId)&&!this.conflicts.has(e))this.publish({uploadIssue:s.path});this.conflicts.add(e)}for(let s of t.files){if(String(s.projectId)===String(this.scope.projectId)&&this.fileStates.get(s.localPath)!==s.state&&["conflict","error","forbidden"].includes(s.state))this.publish({uploadIssue:s.path});this.fileStates.set(s.localPath,s.state)}}if(this.controller.getSnapshot().snapshot?.generation!==this.generation)this.invalid=!0,this.stop(),this.publish({phase:"failed",error:"Drive profile changed while the request was pending",cancelling:!1,stale:!0})})}stop(){if(this.timer)clearTimeout(this.timer);this.timer=void 0,this.unsubscribe?.(),this.unsubscribe=void 0}accept(t){let s=t.status==="SUCCEEDED"?"succeeded":t.status==="FAILED"?"failed":t.status==="CANCELLED"?"cancelled":"running";if(this.publish({job:t,phase:s,error:t.status==="FAILED"?t.error||"Condense job failed":null,cancelling:s==="running"&&this.state.cancelling}),s!=="running")this.stop()}resume=()=>{if(this.invalid||["succeeded","failed","cancelled","unsynced"].includes(this.state.phase)||this.state.phase==="idle"&&!this.state.job)return;if(this.watch(),this.state.phase==="idle"&&this.state.job)this.accept(this.state.job);if(this.state.phase==="running"&&!this.timer&&!this.inFlight)this.schedule()};schedule(){if(this.invalid||this.state.phase!=="running")return;this.timer=setTimeout(()=>{this.timer=void 0,this.poll()},Math.min(30000,2000*2**Math.min(this.failures,4)))}async poll(){if(this.invalid||this.inFlight||!this.state.job)return;this.inFlight=!0;try{let t=await this.controller.request({type:"job",jobId:this.state.job.id});if(t.type!=="job")throw Error("Invalid condense job response");if(this.invalid)return;this.failures=0,this.accept(t.data)}catch(t){if(!this.invalid&&++this.failures>=3)this.publish({error:String(t)})}finally{this.inFlight=!1,this.schedule()}}start=async(t=!1)=>{if(this.invalid||this.inFlight||["uploading","starting","running"].includes(this.state.phase))return;this.watch(),this.inFlight=!0,this.cancelSent=!1,this.publish({phase:t?"starting":"uploading",job:null,unsynced:null,error:null,uploadIssue:null,stale:!1,cancelling:!1});try{if(!this.controller.getSnapshot().snapshot?.running)throw Error("Drive sync is not running");if(!t){let e=await this.controller.request({type:"condensePreflight",projectId:this.scope.projectId,scopePath:this.scope.scopePath});if(e.type!=="unsynced")throw Error("Invalid condense preflight response");if(this.invalid)return;if(this.state.cancelling){this.publish({phase:"cancelled",cancelling:!1});return}if(e.data.pending>0){this.publish({phase:"unsynced",unsynced:e.data});return}}this.publish({phase:"starting"});let s=await this.controller.request({type:"condenseStart",projectId:this.scope.projectId,scopePath:this.scope.scopePath});if(s.type!=="job")throw Error("Invalid condense start response");if(this.invalid)return;if(this.accept(s.data),this.state.cancelling)await this.cancelRemote()}catch(s){if(!this.invalid)this.publish({phase:"failed",error:String(s),cancelling:!1})}finally{if(this.inFlight=!1,this.state.phase==="running")this.schedule();else this.stop()}};async cancelRemote(){if(this.cancelSent||!this.state.job)return;this.cancelSent=!0;try{await this.controller.request({type:"cancelJob",jobId:this.state.job.id})}catch(t){if(this.cancelSent=!1,!this.invalid)this.publish({cancelling:!1,error:String(t)})}}cancel=async()=>{if(this.invalid||this.state.cancelling)return;if(this.publish({cancelling:!0,error:null}),this.state.job)await this.cancelRemote()};adopt=(t)=>{this.watch(),this.accept(t),this.resume()}}function r(t,s,e){let i=h.get(t);if(!i)i=new Map,h.set(t,i);let a=JSON.stringify([t.getSnapshot().snapshot?.generation,s.projectId,s.scopePath]),n=i.get(a);if(!n||e&&n.getSnapshot().job?.id!==e.id)n=new l(t,s,e),i.set(a,n);return n}export{l as CondenseTask,r as condenseTask};
|