@jini-ai/vibecoding 0.6.11 → 0.6.15-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +11 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/project-filesystem.d.ts +36 -0
- package/dist/core/project-filesystem.js +2 -0
- package/dist/core/session.d.ts +115 -0
- package/dist/core/session.js +178 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/paths.d.ts +5 -3
- package/dist/node/paths.js +3 -4
- package/dist/node/project.d.ts +3 -0
- package/dist/node/project.js +3 -3
- package/dist/node/store.d.ts +3 -1
- package/dist/node/store.js +8 -9
- package/dist/react/session.d.ts +2 -1
- package/dist/react/session.js +2 -1
- package/dist/session.d.ts +2 -114
- package/dist/session.js +2 -177
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 506196f: BREAKING (host composition): two native effects are now injected by the host instead of loaded from a sibling
|
|
8
|
+
domain (guard L1 / SI-domain clean-up). No default is provided because a default would reintroduce exactly
|
|
9
|
+
that edge.
|
|
10
|
+
|
|
11
|
+
- `@jini-ai/vibecoding` node: `openFileTreeProject` (and the node store/paths inputs) require
|
|
12
|
+
`filesystem: ProjectFilesystemPort` (`createReader`, `withFileLock`, `writeFileAtomicAsync`,
|
|
13
|
+
`writeJsonFileAtomicAsync`). Bind the existing `@jini-ai/platform` implementations in the host's composition root (see the `ProjectFilesystemPort` doc example).
|
|
14
|
+
Hosts that only use `@jini-ai/vibecoding/html*` are unaffected.
|
|
15
|
+
- `@jini-ai/integrations` saved requests: `SavedRequestDeps` requires `executeCredentialedRequest`;
|
|
16
|
+
pass `makeCredentialedRequest` from `@jini-ai/integrations/credentialed-http` so auditing and redaction stay bound
|
|
17
|
+
to that owner.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [7479159]
|
|
20
|
+
- Updated dependencies [c31264d]
|
|
21
|
+
- Updated dependencies [0728914]
|
|
22
|
+
- Updated dependencies [6ecbdb2]
|
|
23
|
+
- Updated dependencies [63bacc5]
|
|
24
|
+
- @jini-ai/core@0.6.13
|
|
25
|
+
|
|
26
|
+
## 0.6.12
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies
|
|
31
|
+
- @jini-ai/platform@0.6.12
|
|
32
|
+
- @jini-ai/core@0.6.12
|
|
33
|
+
|
|
3
34
|
## 0.6.11
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -159,10 +159,20 @@ belong to the host. Integration verification is deferred by owner directive; see
|
|
|
159
159
|
## Local app projects
|
|
160
160
|
|
|
161
161
|
```ts
|
|
162
|
-
import { openFileTreeProject } from '@jini-ai/vibecoding/node';
|
|
162
|
+
import { openFileTreeProject, type ProjectFilesystemPort } from '@jini-ai/vibecoding/node';
|
|
163
|
+
import { createGuardedFileReader, createNodeGuardedReaderFilesystem, withFileLock,
|
|
164
|
+
writeFileAtomicAsync, writeJsonFileAtomicAsync } from '@jini-ai/platform/fs';
|
|
163
165
|
import { createVibecodingSession } from '@jini-ai/vibecoding/session';
|
|
164
166
|
|
|
167
|
+
// The host binds the existing platform owners once at composition.
|
|
168
|
+
const filesystem: ProjectFilesystemPort = {
|
|
169
|
+
createReader: (input, options = {}) => createGuardedFileReader({
|
|
170
|
+
...input, filesystem: createNodeGuardedReaderFilesystem({}),
|
|
171
|
+
}, options),
|
|
172
|
+
withFileLock, writeFileAtomicAsync, writeJsonFileAtomicAsync,
|
|
173
|
+
};
|
|
165
174
|
const project = await openFileTreeProject({
|
|
175
|
+
filesystem,
|
|
166
176
|
root: appDirectory,
|
|
167
177
|
stateDir: privateHostStateDirectory, // outside appDirectory: dev servers must never serve history
|
|
168
178
|
policy: { allows: ({ relativePath }) => allowedAppPath(relativePath) },
|
package/dist/core/index.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ export { applyEdit, applyEdits, correctionsFor } from "./apply.js";
|
|
|
17
17
|
export type { EditHistory, EditHistoryOptions, HistoryEntry, PartChange } from "./history.js";
|
|
18
18
|
export { createEditHistory } from "./history.js";
|
|
19
19
|
export * from './project.js';
|
|
20
|
+
export type { ProjectFilesystemPort } from './project-filesystem.js';
|
|
20
21
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DenyRules, GuardedFileReader, GuardedReaderLimits } from '@jini-ai/core/filesystem';
|
|
2
|
+
/** Filesystem capabilities required by the local project adapter. Composition binds the existing
|
|
3
|
+
* platform implementations so containment, locking and atomic writes keep a single owner.
|
|
4
|
+
* @example const filesystem: ProjectFilesystemPort = { createReader, withFileLock, writeFileAtomicAsync, writeJsonFileAtomicAsync };
|
|
5
|
+
*/
|
|
6
|
+
export interface ProjectFilesystemPort {
|
|
7
|
+
createReader(required: {
|
|
8
|
+
rootPath: string;
|
|
9
|
+
denyRules: DenyRules;
|
|
10
|
+
limits: GuardedReaderLimits;
|
|
11
|
+
}, options?: {
|
|
12
|
+
excludedListingDirs?: ReadonlySet<string>;
|
|
13
|
+
}): GuardedFileReader;
|
|
14
|
+
withFileLock<T>(required: {
|
|
15
|
+
lockPath: string;
|
|
16
|
+
run: () => Promise<T>;
|
|
17
|
+
}, options?: {
|
|
18
|
+
staleMs?: number;
|
|
19
|
+
createParent?: boolean;
|
|
20
|
+
}): Promise<T>;
|
|
21
|
+
writeFileAtomicAsync(required: {
|
|
22
|
+
filePath: string;
|
|
23
|
+
content: string | Uint8Array;
|
|
24
|
+
}, options?: {
|
|
25
|
+
createParent?: boolean;
|
|
26
|
+
refuseSymlink?: boolean;
|
|
27
|
+
defaultMode?: number;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
writeJsonFileAtomicAsync(required: {
|
|
30
|
+
filePath: string;
|
|
31
|
+
data: unknown;
|
|
32
|
+
}, options?: {
|
|
33
|
+
createParent?: boolean;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=project-filesystem.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module session
|
|
3
|
+
*
|
|
4
|
+
* `VibecodingSession` — the framework-free stateful object over
|
|
5
|
+
* `./core`'s `EditTarget` + `EditHistory`. Everything else in `./react` (the hook, the tool
|
|
6
|
+
* registrations, the presentational components) reads and drives an artifact through this seam
|
|
7
|
+
* rather than touching `EditTarget`/`EditHistory` directly, for one reason: a tool call arrives
|
|
8
|
+
* from the chat's execution path, not from a React event handler, so the thing tool handlers call
|
|
9
|
+
* into cannot be React state — it has to be a plain, subscribable object a `useSyncExternalStore`
|
|
10
|
+
* hook can also read. This module is React-free and exported with the tools from `./session`.
|
|
11
|
+
*
|
|
12
|
+
* ## Why part content is cached rather than re-read on every render
|
|
13
|
+
*
|
|
14
|
+
* `EditTarget.readPart` is a host call — for the `html` target it is cheap (a string slice), but
|
|
15
|
+
* the port makes no such promise for every target (a filesystem or sandbox-backed host could hit a
|
|
16
|
+
* network round trip per part). Re-reading every listed part's content on every `refresh()` would
|
|
17
|
+
* turn "the operator opened the file viewer" into an O(part count) fan-out of host calls, and
|
|
18
|
+
* nothing here can assume that count stays small — see `./target.ts`'s own doc on why
|
|
19
|
+
* `listParts` is the allowlist, not a bound on its size. So `refresh()` only calls `listParts()`;
|
|
20
|
+
* content is fetched lazily, per id, through `readPart()`, and cached. Every mutating operation
|
|
21
|
+
* below (`applyEdits`, `undo`, `redo`, `restoreSnapshot`) in a default session knows the exact before/after
|
|
22
|
+
* content it just wrote — `./apply.ts`'s `ProposedEdit`, `./history.ts`'s
|
|
23
|
+
* `PartChange.before`/`.after` — so the cache is updated from that data directly instead of
|
|
24
|
+
* re-reading the part it just wrote back from the host. Project sessions clear caches instead:
|
|
25
|
+
* their history entries hold revision pointers, and external tools may have changed file bytes.
|
|
26
|
+
*/
|
|
27
|
+
import type { ApplyOutcome, PartId, PartRef, Snapshot } from './types.js';
|
|
28
|
+
import type { EditTarget } from './target.js';
|
|
29
|
+
import { type ProposedEdit } from './apply.js';
|
|
30
|
+
import { type EditHistory, type EditHistoryOptions, type HistoryEntry } from './history.js';
|
|
31
|
+
export interface VibecodingSessionArgs {
|
|
32
|
+
/** The host's artifact port. See `./target.ts`. */
|
|
33
|
+
readonly target: EditTarget;
|
|
34
|
+
}
|
|
35
|
+
export interface VibecodingSessionOptions {
|
|
36
|
+
/** Forwarded to `createEditHistory`. See `./history.ts`'s `EditHistoryOptions`. */
|
|
37
|
+
readonly historyOptions?: EditHistoryOptions;
|
|
38
|
+
/** A project supplies its guarded pointer history instead of a per-file stack. */
|
|
39
|
+
readonly history?: EditHistory;
|
|
40
|
+
/** Whole-turn mutation; owns its history recording and returns aligned outcomes. */
|
|
41
|
+
readonly applyTurn?: (input: {
|
|
42
|
+
readonly edits: readonly ProposedEdit[];
|
|
43
|
+
}, options?: {
|
|
44
|
+
readonly label?: string;
|
|
45
|
+
}) => Promise<readonly ApplyOutcome[]>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Immutable, point-in-time read of a session. `getSnapshot()` returns the same object reference
|
|
49
|
+
* until something actually changes — required for `useSyncExternalStore`, which re-renders a
|
|
50
|
+
* subscriber whenever the reference it reads changes, not whenever it is merely called again.
|
|
51
|
+
*/
|
|
52
|
+
export interface VibecodingSessionSnapshot {
|
|
53
|
+
/** `'idle'` until the first `refresh()` resolves; `'error'` reflects the most recent failure, not
|
|
54
|
+
* a permanently wedged session — a subsequent successful call returns it to `'ready'`. */
|
|
55
|
+
readonly status: 'idle' | 'loading' | 'ready' | 'error';
|
|
56
|
+
/** Every part the host currently advertises, in `EditTarget.listParts()`'s own order. */
|
|
57
|
+
readonly parts: readonly PartRef[];
|
|
58
|
+
/** Cached content, keyed by part id. A part absent from this map has not been read yet — see
|
|
59
|
+
* this module's doc for why content is lazy rather than eagerly fetched for every part. */
|
|
60
|
+
readonly partContent: ReadonlyMap<PartId, string>;
|
|
61
|
+
readonly canUndo: boolean;
|
|
62
|
+
readonly canRedo: boolean;
|
|
63
|
+
/** The most recent operation's failure message, if any. Cleared by the next successful call. */
|
|
64
|
+
readonly lastError: string | undefined;
|
|
65
|
+
}
|
|
66
|
+
export interface ApplyEditsResult {
|
|
67
|
+
readonly outcomes: readonly ApplyOutcome[];
|
|
68
|
+
/** See `./apply.ts`'s `correctionsFor` — the subset a caller feeds back to the model. */
|
|
69
|
+
readonly corrections: readonly {
|
|
70
|
+
readonly id: PartId;
|
|
71
|
+
readonly reason: string;
|
|
72
|
+
}[];
|
|
73
|
+
}
|
|
74
|
+
export interface VibecodingSession {
|
|
75
|
+
/** The underlying port, for a caller (or test) that needs it directly — e.g. to read the whole
|
|
76
|
+
* document a host-specific target stores, which is outside this generic session's vocabulary. */
|
|
77
|
+
readonly target: EditTarget;
|
|
78
|
+
/** The underlying undo/redo stack, for a caller that needs `.entries()` for a history UI. */
|
|
79
|
+
readonly history: EditHistory;
|
|
80
|
+
getSnapshot(): VibecodingSessionSnapshot;
|
|
81
|
+
/** Returns an unsubscribe function; the React hook adapts this object API for React. */
|
|
82
|
+
subscribe(args: {
|
|
83
|
+
readonly listener: () => void;
|
|
84
|
+
}): () => void;
|
|
85
|
+
/** Re-lists parts from the host. Does not re-read content — see this module's doc. */
|
|
86
|
+
refresh(): Promise<void>;
|
|
87
|
+
/** Cached content for `id`, fetching once from the host on first request. */
|
|
88
|
+
readPart(args: {
|
|
89
|
+
readonly id: PartId;
|
|
90
|
+
}): Promise<string>;
|
|
91
|
+
/** Applies `edits` as one undoable transaction and updates the cache from the edits themselves. */
|
|
92
|
+
applyEdits(requiredArgs: {
|
|
93
|
+
readonly edits: readonly ProposedEdit[];
|
|
94
|
+
}, optionalArgs?: {
|
|
95
|
+
readonly label?: string;
|
|
96
|
+
}): Promise<ApplyEditsResult>;
|
|
97
|
+
undo(): Promise<HistoryEntry | null>;
|
|
98
|
+
redo(): Promise<HistoryEntry | null>;
|
|
99
|
+
takeSnapshot(): Promise<Snapshot>;
|
|
100
|
+
restoreSnapshot(args: {
|
|
101
|
+
readonly snapshot: Snapshot;
|
|
102
|
+
}): Promise<HistoryEntry | null>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Builds a session over `target`, with private history by default or injected project history.
|
|
106
|
+
* Injected applyTurn owns whole-turn recording; this session owns notifications and cache invalidation.
|
|
107
|
+
*
|
|
108
|
+
* The returned session starts in `'idle'` with no parts listed — call `refresh()` (or let
|
|
109
|
+
* `useVibecodingSession` do it on mount) to populate it. Nothing here triggers I/O eagerly, so
|
|
110
|
+
* constructing a session has no side effect a caller needs to clean up.
|
|
111
|
+
*
|
|
112
|
+
* @complexity Construction is O(1). See each method for its own cost.
|
|
113
|
+
*/
|
|
114
|
+
export declare function createVibecodingSession({ target }: VibecodingSessionArgs, options?: VibecodingSessionOptions): VibecodingSession;
|
|
115
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { applyEdits as applyEditsToTarget, correctionsFor } from './apply.js';
|
|
2
|
+
import { createEditHistory } from './history.js';
|
|
3
|
+
const READY_ERROR = undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Builds a session over `target`, with private history by default or injected project history.
|
|
6
|
+
* Injected applyTurn owns whole-turn recording; this session owns notifications and cache invalidation.
|
|
7
|
+
*
|
|
8
|
+
* The returned session starts in `'idle'` with no parts listed — call `refresh()` (or let
|
|
9
|
+
* `useVibecodingSession` do it on mount) to populate it. Nothing here triggers I/O eagerly, so
|
|
10
|
+
* constructing a session has no side effect a caller needs to clean up.
|
|
11
|
+
*
|
|
12
|
+
* @complexity Construction is O(1). See each method for its own cost.
|
|
13
|
+
*/
|
|
14
|
+
export function createVibecodingSession({ target }, options = {}) {
|
|
15
|
+
const history = options.history ?? createEditHistory({ target }, options.historyOptions);
|
|
16
|
+
// Project turns write through revision pointers, so cached part content is never reusable after them.
|
|
17
|
+
const applyTurn = options.applyTurn;
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
const partContent = new Map();
|
|
20
|
+
let current = {
|
|
21
|
+
status: 'idle',
|
|
22
|
+
parts: [],
|
|
23
|
+
partContent,
|
|
24
|
+
canUndo: history.canUndo(),
|
|
25
|
+
canRedo: history.canRedo(),
|
|
26
|
+
lastError: READY_ERROR,
|
|
27
|
+
};
|
|
28
|
+
/** Replaces `current` with a fresh object carrying `patch`, then notifies subscribers. A fresh
|
|
29
|
+
* `partContent` Map reference is handed out only when the map's contents actually changed
|
|
30
|
+
* (callers pass a new Map in `patch` for that case); reusing the same Map reference otherwise
|
|
31
|
+
* is what keeps an unrelated status change (e.g. `'loading'` -> `'ready'`) from invalidating a
|
|
32
|
+
* component's memoized read of `partContent`. */
|
|
33
|
+
function commit(patch) {
|
|
34
|
+
current = { ...current, ...patch };
|
|
35
|
+
for (const listener of listeners)
|
|
36
|
+
listener();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs `work`, tagging the session `'loading'` -> `'ready'`/`'error'` and always returning
|
|
40
|
+
* control to the caller — a rejection is recorded as `lastError` AND rethrown, so a component
|
|
41
|
+
* can render `lastError` passively while a tool handler that needs to fail loudly still can.
|
|
42
|
+
*
|
|
43
|
+
* `work` returns its data `patch` alongside its `result` rather than calling `commit` itself, so
|
|
44
|
+
* the success path here can fold that patch into the SAME commit as the `'ready'` status
|
|
45
|
+
* transition — one notification per operation instead of two, which matters to a
|
|
46
|
+
* `useSyncExternalStore` subscriber re-rendering on every commit.
|
|
47
|
+
*/
|
|
48
|
+
async function run(work) {
|
|
49
|
+
commit({ status: 'loading' });
|
|
50
|
+
try {
|
|
51
|
+
const { result, patch } = await work();
|
|
52
|
+
commit({ ...patch, status: 'ready', lastError: READY_ERROR });
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
catch (thrown) {
|
|
56
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
57
|
+
if (applyTurn)
|
|
58
|
+
partContent.clear();
|
|
59
|
+
commit({ status: 'error', lastError: message, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() });
|
|
60
|
+
throw thrown;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Mirrors a replayed change in the cache. Undoing creation keeps empty content because the
|
|
64
|
+
* target's upsert port has no delete operation; see `./history.ts`. */
|
|
65
|
+
function cacheChange(change, side) {
|
|
66
|
+
partContent.set(change.id, side === 'after' ? change.after : change.before);
|
|
67
|
+
}
|
|
68
|
+
/** Mirrors a recorded snapshot restore in the cache and re-reads the part list it may have grown. */
|
|
69
|
+
async function restoredPatch(entry) {
|
|
70
|
+
if (applyTurn)
|
|
71
|
+
partContent.clear();
|
|
72
|
+
else
|
|
73
|
+
for (const change of entry.changes)
|
|
74
|
+
cacheChange(change, 'after');
|
|
75
|
+
const parts = await target.listParts();
|
|
76
|
+
return { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
target,
|
|
80
|
+
history,
|
|
81
|
+
getSnapshot: () => current,
|
|
82
|
+
subscribe({ listener }) {
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
return () => listeners.delete(listener);
|
|
85
|
+
},
|
|
86
|
+
refresh: () => run(async () => {
|
|
87
|
+
const parts = await target.listParts();
|
|
88
|
+
if (applyTurn)
|
|
89
|
+
partContent.clear();
|
|
90
|
+
const known = new Set(parts.map((p) => p.id));
|
|
91
|
+
// Drop cached content for parts the host no longer lists — stale content for an
|
|
92
|
+
// unaddressable id would otherwise linger forever with nothing to invalidate it.
|
|
93
|
+
for (const id of partContent.keys())
|
|
94
|
+
if (!known.has(id))
|
|
95
|
+
partContent.delete(id);
|
|
96
|
+
return {
|
|
97
|
+
result: undefined,
|
|
98
|
+
patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
|
|
99
|
+
};
|
|
100
|
+
}),
|
|
101
|
+
readPart: ({ id }) => run(async () => {
|
|
102
|
+
const cached = applyTurn ? undefined : partContent.get(id);
|
|
103
|
+
if (cached !== undefined)
|
|
104
|
+
return { result: cached, patch: {} };
|
|
105
|
+
if (applyTurn)
|
|
106
|
+
partContent.clear();
|
|
107
|
+
const content = await target.readPart({ id });
|
|
108
|
+
partContent.set(id, content);
|
|
109
|
+
return { result: content, patch: { partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() } };
|
|
110
|
+
}),
|
|
111
|
+
applyEdits: ({ edits }, options = {}) => run(async () => {
|
|
112
|
+
const outcomes = await (applyTurn
|
|
113
|
+
? applyTurn({ edits }, options)
|
|
114
|
+
: history.transaction({ work: (recording) => applyEditsToTarget({ target: recording, edits }) }, options));
|
|
115
|
+
if (applyTurn)
|
|
116
|
+
partContent.clear();
|
|
117
|
+
// Outcomes are aligned with proposals, including repeated ids. Matching only by id
|
|
118
|
+
// would cache the first proposal instead of the last successful write for that part.
|
|
119
|
+
// This pass is O(edits.length), with no additional reads through the target port.
|
|
120
|
+
for (const [index, outcome] of outcomes.entries()) {
|
|
121
|
+
if (outcome.status !== 'applied')
|
|
122
|
+
continue;
|
|
123
|
+
const edit = edits[index];
|
|
124
|
+
if (edit && !applyTurn)
|
|
125
|
+
partContent.set(edit.id, edit.content);
|
|
126
|
+
}
|
|
127
|
+
const parts = await target.listParts();
|
|
128
|
+
return {
|
|
129
|
+
result: { outcomes, corrections: correctionsFor({ outcomes }) },
|
|
130
|
+
patch: {
|
|
131
|
+
parts,
|
|
132
|
+
partContent: new Map(partContent),
|
|
133
|
+
canUndo: history.canUndo(),
|
|
134
|
+
canRedo: history.canRedo(),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}),
|
|
138
|
+
undo: () => run(async () => {
|
|
139
|
+
const entry = await history.undo();
|
|
140
|
+
if (!entry)
|
|
141
|
+
return { result: entry, patch: {} };
|
|
142
|
+
// Undo replays in reverse order. Repeated ids must leave the earliest before value
|
|
143
|
+
// cached, matching what history wrote to the target.
|
|
144
|
+
if (applyTurn)
|
|
145
|
+
partContent.clear();
|
|
146
|
+
else
|
|
147
|
+
for (const change of [...entry.changes].reverse())
|
|
148
|
+
cacheChange(change, 'before');
|
|
149
|
+
const parts = applyTurn ? await target.listParts() : current.parts;
|
|
150
|
+
return {
|
|
151
|
+
result: entry,
|
|
152
|
+
patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
redo: () => run(async () => {
|
|
156
|
+
const entry = await history.redo();
|
|
157
|
+
if (!entry)
|
|
158
|
+
return { result: entry, patch: {} };
|
|
159
|
+
if (applyTurn)
|
|
160
|
+
partContent.clear();
|
|
161
|
+
else
|
|
162
|
+
for (const change of entry.changes)
|
|
163
|
+
cacheChange(change, 'after');
|
|
164
|
+
const parts = applyTurn ? await target.listParts() : current.parts;
|
|
165
|
+
return {
|
|
166
|
+
result: entry,
|
|
167
|
+
patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
|
|
168
|
+
};
|
|
169
|
+
}),
|
|
170
|
+
takeSnapshot: () => run(async () => ({ result: await target.snapshot(), patch: {} })),
|
|
171
|
+
restoreSnapshot: ({ snapshot }) => run(async () => {
|
|
172
|
+
const entry = await history.restore({ snapshot });
|
|
173
|
+
// A restore that already matched records no entry and changes nothing to mirror.
|
|
174
|
+
return { result: entry, patch: entry ? await restoredPatch(entry) : {} };
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=session.js.map
|
package/dist/node/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { openFileTreeProject } from './project.js';
|
|
|
3
3
|
export type { OpenFileTreeProjectInput, OpenFileTreeProjectOptions } from './project.js';
|
|
4
4
|
export type { FileTreePolicy } from './paths.js';
|
|
5
5
|
export * from '../core/project.js';
|
|
6
|
+
export type { ProjectFilesystemPort } from '../core/project-filesystem.js';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/node/paths.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectFilesystemPort } from '../core/project-filesystem.js';
|
|
1
2
|
import { type ProjectLimits } from '../core/project.js';
|
|
2
3
|
export interface FileTreePolicy {
|
|
3
4
|
/** Host authority intersects mandatory exclusions, including for external captures. */
|
|
@@ -30,15 +31,16 @@ export declare function canonicalPath({ relativePath, maxDepth }: {
|
|
|
30
31
|
export declare function assertCandidatePaths({ paths }: {
|
|
31
32
|
paths: readonly string[];
|
|
32
33
|
}, _options?: {}): void;
|
|
33
|
-
/**
|
|
34
|
+
/** The injected reader owns physical containment and bounded traversal; this adapter adds project policy.
|
|
34
35
|
* @complexity Construction O(1); path checks O(depth), listing bounded by configured limits.
|
|
35
36
|
*/
|
|
36
|
-
export declare function createProjectPaths({ root, policy, limits }: {
|
|
37
|
+
export declare function createProjectPaths({ root, policy, limits, filesystem }: {
|
|
37
38
|
root: string;
|
|
38
39
|
policy: FileTreePolicy;
|
|
39
40
|
limits: ProjectLimits;
|
|
41
|
+
filesystem: ProjectFilesystemPort;
|
|
40
42
|
}, _options?: {}): {
|
|
41
|
-
reader: import("@jini-ai/
|
|
43
|
+
reader: import("@jini-ai/core/filesystem").GuardedFileReader;
|
|
42
44
|
check: ({ relativePath }: {
|
|
43
45
|
relativePath: string;
|
|
44
46
|
}) => string;
|
package/dist/node/paths.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createGuardedFileReader, createNodeGuardedReaderFilesystem } from '@jini-ai/platform/fs';
|
|
2
1
|
import { ProjectError } from '../core/project.js';
|
|
3
2
|
export const DEFAULT_PROJECT_LIMITS = {
|
|
4
3
|
maxFiles: 2_000, maxFileBytes: 2 * 1024 * 1024, maxTreeBytes: 64 * 1024 * 1024,
|
|
@@ -65,11 +64,11 @@ export function assertCandidatePaths({ paths }, _options = {}) {
|
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
|
-
/**
|
|
67
|
+
/** The injected reader owns physical containment and bounded traversal; this adapter adds project policy.
|
|
69
68
|
* @complexity Construction O(1); path checks O(depth), listing bounded by configured limits.
|
|
70
69
|
*/
|
|
71
|
-
export function createProjectPaths({ root, policy, limits }, _options = {}) {
|
|
72
|
-
const reader =
|
|
70
|
+
export function createProjectPaths({ root, policy, limits, filesystem }, _options = {}) {
|
|
71
|
+
const reader = filesystem.createReader({ rootPath: root,
|
|
73
72
|
denyRules: { segments: EXCLUDED, filenamePatterns: [/^\.env/i, ATOMIC_TEMP] },
|
|
74
73
|
limits: { maxFileBytes: limits.maxFileBytes, binarySniffBytes: 1024,
|
|
75
74
|
maxListedFiles: limits.maxFiles + 1, maxWalkDepth: limits.maxDepth, maxWalkEntries: limits.maxFiles * (limits.maxDepth + 1) },
|
package/dist/node/project.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { ProjectFilesystemPort } from '../core/project-filesystem.js';
|
|
1
2
|
import { type FileTreeProject, type ProjectLimits, type ProjectValidator } from '../core/project.js';
|
|
2
3
|
import { type FileTreePolicy } from './paths.js';
|
|
3
4
|
export interface OpenFileTreeProjectInput {
|
|
5
|
+
/** Native effects are supplied by the host composition root. */
|
|
6
|
+
readonly filesystem: ProjectFilesystemPort;
|
|
4
7
|
readonly root: string;
|
|
5
8
|
/** Private host-owned directory outside the watched and served application tree. */
|
|
6
9
|
readonly stateDir: string;
|
package/dist/node/project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createEditHistory } from '../core/history.js';
|
|
2
|
-
import { FsFilePathError } from '@jini-ai/
|
|
2
|
+
import { FsFilePathError } from '@jini-ai/core/filesystem';
|
|
3
3
|
import { ProjectError } from '../core/project.js';
|
|
4
4
|
import { assertCandidatePaths, createProjectPaths, projectLimits } from './paths.js';
|
|
5
5
|
import { createProjectStore, prepareDirectories, revisionToken } from './store.js';
|
|
@@ -117,8 +117,8 @@ export async function openFileTreeProject(input, options = {}) {
|
|
|
117
117
|
throw new TypeError('historyLimit must be positive');
|
|
118
118
|
try {
|
|
119
119
|
const directories = await prepareDirectories(input);
|
|
120
|
-
const paths = createProjectPaths({ root: directories.root, policy: input.policy, limits });
|
|
121
|
-
const store = createProjectStore({ ...directories, paths, limits });
|
|
120
|
+
const paths = createProjectPaths({ root: directories.root, policy: input.policy, limits, filesystem: input.filesystem });
|
|
121
|
+
const store = createProjectStore({ ...directories, paths, limits, filesystem: input.filesystem });
|
|
122
122
|
const project = assembleProject({ store, paths, limits, historyLimit, validator: input.validator });
|
|
123
123
|
await project.capture();
|
|
124
124
|
return project;
|
package/dist/node/store.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectFilesystemPort } from '../core/project-filesystem.js';
|
|
1
2
|
import { type ProjectLimits, type RevisionToken } from '../core/project.js';
|
|
2
3
|
import { type ProjectPaths } from './paths.js';
|
|
3
4
|
export interface ManifestFile {
|
|
@@ -30,6 +31,7 @@ export interface StoreInput {
|
|
|
30
31
|
stateDir: string;
|
|
31
32
|
paths: ProjectPaths;
|
|
32
33
|
limits: ProjectLimits;
|
|
34
|
+
filesystem: ProjectFilesystemPort;
|
|
33
35
|
}
|
|
34
36
|
export interface StoreOptions {
|
|
35
37
|
/** Internal deterministic process-crash seam; never needed by the public factory. */
|
|
@@ -55,7 +57,7 @@ export declare function prepareDirectories({ root, stateDir }: {
|
|
|
55
57
|
/** Content-addressed persistence and idempotent roll-forward. Call mutations inside locked().
|
|
56
58
|
* @complexity O(bytes + files log files + files × depth²) for capture; memory bounded by maxTreeBytes.
|
|
57
59
|
*/
|
|
58
|
-
export declare function createProjectStore({ root, stateDir, paths, limits }: StoreInput, options?: StoreOptions): {
|
|
60
|
+
export declare function createProjectStore({ root, stateDir, paths, limits, filesystem }: StoreInput, options?: StoreOptions): {
|
|
59
61
|
readCurrent: () => Promise<Current | null>;
|
|
60
62
|
manifest: ({ revisionId }: {
|
|
61
63
|
revisionId: string;
|
package/dist/node/store.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import { withFileLock, writeFileAtomicAsync, writeJsonFileAtomicAsync } from '@jini-ai/platform/fs';
|
|
5
4
|
import { ProjectError } from '../core/project.js';
|
|
6
5
|
import { assertCandidatePaths, portablePathKey } from './paths.js';
|
|
7
6
|
function fingerprint(info) {
|
|
@@ -169,7 +168,7 @@ export async function prepareDirectories({ root, stateDir }, _options = {}) {
|
|
|
169
168
|
/** Content-addressed persistence and idempotent roll-forward. Call mutations inside locked().
|
|
170
169
|
* @complexity O(bytes + files log files + files × depth²) for capture; memory bounded by maxTreeBytes.
|
|
171
170
|
*/
|
|
172
|
-
export function createProjectStore({ root, stateDir, paths, limits }, options = {}) {
|
|
171
|
+
export function createProjectStore({ root, stateDir, paths, limits, filesystem }, options = {}) {
|
|
173
172
|
const currentPath = resolve(stateDir, 'current.json');
|
|
174
173
|
const objectPath = (hash) => { assertHash(hash); return resolve(stateDir, 'objects', hash); };
|
|
175
174
|
const manifestPath = (hash) => { assertHash(hash); return resolve(stateDir, 'manifests', `${hash}.json`); };
|
|
@@ -223,7 +222,7 @@ export function createProjectStore({ root, stateDir, paths, limits }, options =
|
|
|
223
222
|
if (existing)
|
|
224
223
|
await manifest({ revisionId });
|
|
225
224
|
else
|
|
226
|
-
await writeJsonFileAtomicAsync({ filePath: manifestPath(revisionId), data: value }, { createParent: true });
|
|
225
|
+
await filesystem.writeJsonFileAtomicAsync({ filePath: manifestPath(revisionId), data: value }, { createParent: true });
|
|
227
226
|
await checkpoint('manifest');
|
|
228
227
|
return revisionId;
|
|
229
228
|
}
|
|
@@ -232,7 +231,7 @@ export function createProjectStore({ root, stateDir, paths, limits }, options =
|
|
|
232
231
|
if (exists)
|
|
233
232
|
await object({ file });
|
|
234
233
|
else
|
|
235
|
-
await writeFileAtomicAsync({ filePath: objectPath(file.hash), content: bytes }, { createParent: true });
|
|
234
|
+
await filesystem.writeFileAtomicAsync({ filePath: objectPath(file.hash), content: bytes }, { createParent: true });
|
|
236
235
|
}
|
|
237
236
|
async function observations({ files }) {
|
|
238
237
|
const result = Object.create(null);
|
|
@@ -348,12 +347,12 @@ export function createProjectStore({ root, stateDir, paths, limits }, options =
|
|
|
348
347
|
for (const file of writes)
|
|
349
348
|
await removeBlockingFolder({ path: file.path, before });
|
|
350
349
|
for (const file of writes) {
|
|
351
|
-
await writeFileAtomicAsync({ filePath: await regularPath({ paths, path: file.path }), content: bytes.get(file.path) }, { createParent: true, refuseSymlink: true, defaultMode: 0o644 });
|
|
350
|
+
await filesystem.writeFileAtomicAsync({ filePath: await regularPath({ paths, path: file.path }), content: bytes.get(file.path) }, { createParent: true, refuseSymlink: true, defaultMode: 0o644 });
|
|
352
351
|
await checkpoint('write');
|
|
353
352
|
}
|
|
354
353
|
await prune({ before, after });
|
|
355
354
|
const settled = { ...current, pending: null, observed: await observations(after) };
|
|
356
|
-
await writeJsonFileAtomicAsync({ filePath: currentPath, data: settled });
|
|
355
|
+
await filesystem.writeJsonFileAtomicAsync({ filePath: currentPath, data: settled });
|
|
357
356
|
await checkpoint('clear');
|
|
358
357
|
return settled;
|
|
359
358
|
}
|
|
@@ -435,7 +434,7 @@ export function createProjectStore({ root, stateDir, paths, limits }, options =
|
|
|
435
434
|
throw new ProjectError({ code: 'LIMIT', message: 'generation exhausted' });
|
|
436
435
|
const next = { version: 1, root, revision: revisionId, generation: (current?.generation ?? 0) + 1,
|
|
437
436
|
pending: captured ? null : { from: current?.revision ?? null }, observed: captured ?? {} };
|
|
438
|
-
await writeJsonFileAtomicAsync({ filePath: currentPath, data: next });
|
|
437
|
+
await filesystem.writeJsonFileAtomicAsync({ filePath: currentPath, data: next });
|
|
439
438
|
await checkpoint('journal');
|
|
440
439
|
return captured ? next : materialize({ current: next });
|
|
441
440
|
}
|
|
@@ -444,7 +443,7 @@ export function createProjectStore({ root, stateDir, paths, limits }, options =
|
|
|
444
443
|
if (JSON.stringify(current.observed) === JSON.stringify(observed))
|
|
445
444
|
return current;
|
|
446
445
|
const next = { ...current, observed };
|
|
447
|
-
await writeJsonFileAtomicAsync({ filePath: currentPath, data: next });
|
|
446
|
+
await filesystem.writeJsonFileAtomicAsync({ filePath: currentPath, data: next });
|
|
448
447
|
return next;
|
|
449
448
|
}
|
|
450
449
|
/** Both stacks pin manifests and blobs. Expired snapshots do not pin storage indefinitely. */
|
|
@@ -459,7 +458,7 @@ export function createProjectStore({ root, stateDir, paths, limits }, options =
|
|
|
459
458
|
}
|
|
460
459
|
return {
|
|
461
460
|
readCurrent, manifest, object, prepare, scan, select, collect, preflight, observe, readWorkingFile,
|
|
462
|
-
locked: (run) => withFileLock({ lockPath: resolve(stateDir, 'project.lock'), run }, { staleMs: Infinity, createParent: true }),
|
|
461
|
+
locked: (run) => filesystem.withFileLock({ lockPath: resolve(stateDir, 'project.lock'), run }, { staleMs: Infinity, createParent: true }),
|
|
463
462
|
recover,
|
|
464
463
|
};
|
|
465
464
|
}
|
package/dist/react/session.d.ts
CHANGED
package/dist/react/session.js
CHANGED
package/dist/session.d.ts
CHANGED
|
@@ -1,116 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `VibecodingSession` — the framework-free stateful object over
|
|
5
|
-
* `./core`'s `EditTarget` + `EditHistory`. Everything else in `./react` (the hook, the tool
|
|
6
|
-
* registrations, the presentational components) reads and drives an artifact through this seam
|
|
7
|
-
* rather than touching `EditTarget`/`EditHistory` directly, for one reason: a tool call arrives
|
|
8
|
-
* from the chat's execution path, not from a React event handler, so the thing tool handlers call
|
|
9
|
-
* into cannot be React state — it has to be a plain, subscribable object a `useSyncExternalStore`
|
|
10
|
-
* hook can also read. This module is React-free and exported with the tools from `./session`.
|
|
11
|
-
*
|
|
12
|
-
* ## Why part content is cached rather than re-read on every render
|
|
13
|
-
*
|
|
14
|
-
* `EditTarget.readPart` is a host call — for the `html` target it is cheap (a string slice), but
|
|
15
|
-
* the port makes no such promise for every target (a filesystem or sandbox-backed host could hit a
|
|
16
|
-
* network round trip per part). Re-reading every listed part's content on every `refresh()` would
|
|
17
|
-
* turn "the operator opened the file viewer" into an O(part count) fan-out of host calls, and
|
|
18
|
-
* nothing here can assume that count stays small — see `./core/target.ts`'s own doc on why
|
|
19
|
-
* `listParts` is the allowlist, not a bound on its size. So `refresh()` only calls `listParts()`;
|
|
20
|
-
* content is fetched lazily, per id, through `readPart()`, and cached. Every mutating operation
|
|
21
|
-
* below (`applyEdits`, `undo`, `redo`, `restoreSnapshot`) in a default session knows the exact before/after
|
|
22
|
-
* content it just wrote — `./core/apply.ts`'s `ProposedEdit`, `./core/history.ts`'s
|
|
23
|
-
* `PartChange.before`/`.after` — so the cache is updated from that data directly instead of
|
|
24
|
-
* re-reading the part it just wrote back from the host. Project sessions clear caches instead:
|
|
25
|
-
* their history entries hold revision pointers, and external tools may have changed file bytes.
|
|
26
|
-
*/
|
|
27
|
-
import type { ApplyOutcome, PartId, PartRef, Snapshot } from './core/types.js';
|
|
28
|
-
import type { EditTarget } from './core/target.js';
|
|
29
|
-
import { type ProposedEdit } from './core/apply.js';
|
|
30
|
-
import { type EditHistory, type EditHistoryOptions, type HistoryEntry } from './core/history.js';
|
|
31
|
-
export interface VibecodingSessionArgs {
|
|
32
|
-
/** The host's artifact port. See `./core/target.ts`. */
|
|
33
|
-
readonly target: EditTarget;
|
|
34
|
-
}
|
|
35
|
-
export interface VibecodingSessionOptions {
|
|
36
|
-
/** Forwarded to `createEditHistory`. See `./core/history.ts`'s `EditHistoryOptions`. */
|
|
37
|
-
readonly historyOptions?: EditHistoryOptions;
|
|
38
|
-
/** A project supplies its guarded pointer history instead of a per-file stack. */
|
|
39
|
-
readonly history?: EditHistory;
|
|
40
|
-
/** Whole-turn mutation; owns its history recording and returns aligned outcomes. */
|
|
41
|
-
readonly applyTurn?: (input: {
|
|
42
|
-
readonly edits: readonly ProposedEdit[];
|
|
43
|
-
}, options?: {
|
|
44
|
-
readonly label?: string;
|
|
45
|
-
}) => Promise<readonly ApplyOutcome[]>;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Immutable, point-in-time read of a session. `getSnapshot()` returns the same object reference
|
|
49
|
-
* until something actually changes — required for `useSyncExternalStore`, which re-renders a
|
|
50
|
-
* subscriber whenever the reference it reads changes, not whenever it is merely called again.
|
|
51
|
-
*/
|
|
52
|
-
export interface VibecodingSessionSnapshot {
|
|
53
|
-
/** `'idle'` until the first `refresh()` resolves; `'error'` reflects the most recent failure, not
|
|
54
|
-
* a permanently wedged session — a subsequent successful call returns it to `'ready'`. */
|
|
55
|
-
readonly status: 'idle' | 'loading' | 'ready' | 'error';
|
|
56
|
-
/** Every part the host currently advertises, in `EditTarget.listParts()`'s own order. */
|
|
57
|
-
readonly parts: readonly PartRef[];
|
|
58
|
-
/** Cached content, keyed by part id. A part absent from this map has not been read yet — see
|
|
59
|
-
* this module's doc for why content is lazy rather than eagerly fetched for every part. */
|
|
60
|
-
readonly partContent: ReadonlyMap<PartId, string>;
|
|
61
|
-
readonly canUndo: boolean;
|
|
62
|
-
readonly canRedo: boolean;
|
|
63
|
-
/** The most recent operation's failure message, if any. Cleared by the next successful call. */
|
|
64
|
-
readonly lastError: string | undefined;
|
|
65
|
-
}
|
|
66
|
-
export interface ApplyEditsResult {
|
|
67
|
-
readonly outcomes: readonly ApplyOutcome[];
|
|
68
|
-
/** See `./core/apply.ts`'s `correctionsFor` — the subset a caller feeds back to the model. */
|
|
69
|
-
readonly corrections: readonly {
|
|
70
|
-
readonly id: PartId;
|
|
71
|
-
readonly reason: string;
|
|
72
|
-
}[];
|
|
73
|
-
}
|
|
74
|
-
export interface VibecodingSession {
|
|
75
|
-
/** The underlying port, for a caller (or test) that needs it directly — e.g. to read the whole
|
|
76
|
-
* document a host-specific target stores, which is outside this generic session's vocabulary. */
|
|
77
|
-
readonly target: EditTarget;
|
|
78
|
-
/** The underlying undo/redo stack, for a caller that needs `.entries()` for a history UI. */
|
|
79
|
-
readonly history: EditHistory;
|
|
80
|
-
getSnapshot(): VibecodingSessionSnapshot;
|
|
81
|
-
/** Returns an unsubscribe function; the React hook adapts this object API for React. */
|
|
82
|
-
subscribe(args: {
|
|
83
|
-
readonly listener: () => void;
|
|
84
|
-
}): () => void;
|
|
85
|
-
/** Re-lists parts from the host. Does not re-read content — see this module's doc. */
|
|
86
|
-
refresh(): Promise<void>;
|
|
87
|
-
/** Cached content for `id`, fetching once from the host on first request. */
|
|
88
|
-
readPart(args: {
|
|
89
|
-
readonly id: PartId;
|
|
90
|
-
}): Promise<string>;
|
|
91
|
-
/** Applies `edits` as one undoable transaction and updates the cache from the edits themselves. */
|
|
92
|
-
applyEdits(requiredArgs: {
|
|
93
|
-
readonly edits: readonly ProposedEdit[];
|
|
94
|
-
}, optionalArgs?: {
|
|
95
|
-
readonly label?: string;
|
|
96
|
-
}): Promise<ApplyEditsResult>;
|
|
97
|
-
undo(): Promise<HistoryEntry | null>;
|
|
98
|
-
redo(): Promise<HistoryEntry | null>;
|
|
99
|
-
takeSnapshot(): Promise<Snapshot>;
|
|
100
|
-
restoreSnapshot(args: {
|
|
101
|
-
readonly snapshot: Snapshot;
|
|
102
|
-
}): Promise<HistoryEntry | null>;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Builds a session over `target`, with private history by default or injected project history.
|
|
106
|
-
* Injected applyTurn owns whole-turn recording; this session owns notifications and cache invalidation.
|
|
107
|
-
*
|
|
108
|
-
* The returned session starts in `'idle'` with no parts listed — call `refresh()` (or let
|
|
109
|
-
* `useVibecodingSession` do it on mount) to populate it. Nothing here triggers I/O eagerly, so
|
|
110
|
-
* constructing a session has no side effect a caller needs to clean up.
|
|
111
|
-
*
|
|
112
|
-
* @complexity Construction is O(1). See each method for its own cost.
|
|
113
|
-
*/
|
|
114
|
-
export declare function createVibecodingSession({ target }: VibecodingSessionArgs, options?: VibecodingSessionOptions): VibecodingSession;
|
|
1
|
+
/** Compatibility path; the framework-free session is shared with the React integration. */
|
|
2
|
+
export * from "./core/session.js";
|
|
115
3
|
export * from "./tools.js";
|
|
116
4
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.js
CHANGED
|
@@ -1,179 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const READY_ERROR = undefined;
|
|
4
|
-
/**
|
|
5
|
-
* Builds a session over `target`, with private history by default or injected project history.
|
|
6
|
-
* Injected applyTurn owns whole-turn recording; this session owns notifications and cache invalidation.
|
|
7
|
-
*
|
|
8
|
-
* The returned session starts in `'idle'` with no parts listed — call `refresh()` (or let
|
|
9
|
-
* `useVibecodingSession` do it on mount) to populate it. Nothing here triggers I/O eagerly, so
|
|
10
|
-
* constructing a session has no side effect a caller needs to clean up.
|
|
11
|
-
*
|
|
12
|
-
* @complexity Construction is O(1). See each method for its own cost.
|
|
13
|
-
*/
|
|
14
|
-
export function createVibecodingSession({ target }, options = {}) {
|
|
15
|
-
const history = options.history ?? createEditHistory({ target }, options.historyOptions);
|
|
16
|
-
// Project turns write through revision pointers, so cached part content is never reusable after them.
|
|
17
|
-
const applyTurn = options.applyTurn;
|
|
18
|
-
const listeners = new Set();
|
|
19
|
-
const partContent = new Map();
|
|
20
|
-
let current = {
|
|
21
|
-
status: 'idle',
|
|
22
|
-
parts: [],
|
|
23
|
-
partContent,
|
|
24
|
-
canUndo: history.canUndo(),
|
|
25
|
-
canRedo: history.canRedo(),
|
|
26
|
-
lastError: READY_ERROR,
|
|
27
|
-
};
|
|
28
|
-
/** Replaces `current` with a fresh object carrying `patch`, then notifies subscribers. A fresh
|
|
29
|
-
* `partContent` Map reference is handed out only when the map's contents actually changed
|
|
30
|
-
* (callers pass a new Map in `patch` for that case); reusing the same Map reference otherwise
|
|
31
|
-
* is what keeps an unrelated status change (e.g. `'loading'` -> `'ready'`) from invalidating a
|
|
32
|
-
* component's memoized read of `partContent`. */
|
|
33
|
-
function commit(patch) {
|
|
34
|
-
current = { ...current, ...patch };
|
|
35
|
-
for (const listener of listeners)
|
|
36
|
-
listener();
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Runs `work`, tagging the session `'loading'` -> `'ready'`/`'error'` and always returning
|
|
40
|
-
* control to the caller — a rejection is recorded as `lastError` AND rethrown, so a component
|
|
41
|
-
* can render `lastError` passively while a tool handler that needs to fail loudly still can.
|
|
42
|
-
*
|
|
43
|
-
* `work` returns its data `patch` alongside its `result` rather than calling `commit` itself, so
|
|
44
|
-
* the success path here can fold that patch into the SAME commit as the `'ready'` status
|
|
45
|
-
* transition — one notification per operation instead of two, which matters to a
|
|
46
|
-
* `useSyncExternalStore` subscriber re-rendering on every commit.
|
|
47
|
-
*/
|
|
48
|
-
async function run(work) {
|
|
49
|
-
commit({ status: 'loading' });
|
|
50
|
-
try {
|
|
51
|
-
const { result, patch } = await work();
|
|
52
|
-
commit({ ...patch, status: 'ready', lastError: READY_ERROR });
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
catch (thrown) {
|
|
56
|
-
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
57
|
-
if (applyTurn)
|
|
58
|
-
partContent.clear();
|
|
59
|
-
commit({ status: 'error', lastError: message, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() });
|
|
60
|
-
throw thrown;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/** Mirrors a replayed change in the cache. Undoing creation keeps empty content because the
|
|
64
|
-
* target's upsert port has no delete operation; see `./core/history.ts`. */
|
|
65
|
-
function cacheChange(change, side) {
|
|
66
|
-
partContent.set(change.id, side === 'after' ? change.after : change.before);
|
|
67
|
-
}
|
|
68
|
-
/** Mirrors a recorded snapshot restore in the cache and re-reads the part list it may have grown. */
|
|
69
|
-
async function restoredPatch(entry) {
|
|
70
|
-
if (applyTurn)
|
|
71
|
-
partContent.clear();
|
|
72
|
-
else
|
|
73
|
-
for (const change of entry.changes)
|
|
74
|
-
cacheChange(change, 'after');
|
|
75
|
-
const parts = await target.listParts();
|
|
76
|
-
return { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() };
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
target,
|
|
80
|
-
history,
|
|
81
|
-
getSnapshot: () => current,
|
|
82
|
-
subscribe({ listener }) {
|
|
83
|
-
listeners.add(listener);
|
|
84
|
-
return () => listeners.delete(listener);
|
|
85
|
-
},
|
|
86
|
-
refresh: () => run(async () => {
|
|
87
|
-
const parts = await target.listParts();
|
|
88
|
-
if (applyTurn)
|
|
89
|
-
partContent.clear();
|
|
90
|
-
const known = new Set(parts.map((p) => p.id));
|
|
91
|
-
// Drop cached content for parts the host no longer lists — stale content for an
|
|
92
|
-
// unaddressable id would otherwise linger forever with nothing to invalidate it.
|
|
93
|
-
for (const id of partContent.keys())
|
|
94
|
-
if (!known.has(id))
|
|
95
|
-
partContent.delete(id);
|
|
96
|
-
return {
|
|
97
|
-
result: undefined,
|
|
98
|
-
patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
|
|
99
|
-
};
|
|
100
|
-
}),
|
|
101
|
-
readPart: ({ id }) => run(async () => {
|
|
102
|
-
const cached = applyTurn ? undefined : partContent.get(id);
|
|
103
|
-
if (cached !== undefined)
|
|
104
|
-
return { result: cached, patch: {} };
|
|
105
|
-
if (applyTurn)
|
|
106
|
-
partContent.clear();
|
|
107
|
-
const content = await target.readPart({ id });
|
|
108
|
-
partContent.set(id, content);
|
|
109
|
-
return { result: content, patch: { partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() } };
|
|
110
|
-
}),
|
|
111
|
-
applyEdits: ({ edits }, options = {}) => run(async () => {
|
|
112
|
-
const outcomes = await (applyTurn
|
|
113
|
-
? applyTurn({ edits }, options)
|
|
114
|
-
: history.transaction({ work: (recording) => applyEditsToTarget({ target: recording, edits }) }, options));
|
|
115
|
-
if (applyTurn)
|
|
116
|
-
partContent.clear();
|
|
117
|
-
// Outcomes are aligned with proposals, including repeated ids. Matching only by id
|
|
118
|
-
// would cache the first proposal instead of the last successful write for that part.
|
|
119
|
-
// This pass is O(edits.length), with no additional reads through the target port.
|
|
120
|
-
for (const [index, outcome] of outcomes.entries()) {
|
|
121
|
-
if (outcome.status !== 'applied')
|
|
122
|
-
continue;
|
|
123
|
-
const edit = edits[index];
|
|
124
|
-
if (edit && !applyTurn)
|
|
125
|
-
partContent.set(edit.id, edit.content);
|
|
126
|
-
}
|
|
127
|
-
const parts = await target.listParts();
|
|
128
|
-
return {
|
|
129
|
-
result: { outcomes, corrections: correctionsFor({ outcomes }) },
|
|
130
|
-
patch: {
|
|
131
|
-
parts,
|
|
132
|
-
partContent: new Map(partContent),
|
|
133
|
-
canUndo: history.canUndo(),
|
|
134
|
-
canRedo: history.canRedo(),
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}),
|
|
138
|
-
undo: () => run(async () => {
|
|
139
|
-
const entry = await history.undo();
|
|
140
|
-
if (!entry)
|
|
141
|
-
return { result: entry, patch: {} };
|
|
142
|
-
// Undo replays in reverse order. Repeated ids must leave the earliest before value
|
|
143
|
-
// cached, matching what history wrote to the target.
|
|
144
|
-
if (applyTurn)
|
|
145
|
-
partContent.clear();
|
|
146
|
-
else
|
|
147
|
-
for (const change of [...entry.changes].reverse())
|
|
148
|
-
cacheChange(change, 'before');
|
|
149
|
-
const parts = applyTurn ? await target.listParts() : current.parts;
|
|
150
|
-
return {
|
|
151
|
-
result: entry,
|
|
152
|
-
patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
|
|
153
|
-
};
|
|
154
|
-
}),
|
|
155
|
-
redo: () => run(async () => {
|
|
156
|
-
const entry = await history.redo();
|
|
157
|
-
if (!entry)
|
|
158
|
-
return { result: entry, patch: {} };
|
|
159
|
-
if (applyTurn)
|
|
160
|
-
partContent.clear();
|
|
161
|
-
else
|
|
162
|
-
for (const change of entry.changes)
|
|
163
|
-
cacheChange(change, 'after');
|
|
164
|
-
const parts = applyTurn ? await target.listParts() : current.parts;
|
|
165
|
-
return {
|
|
166
|
-
result: entry,
|
|
167
|
-
patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
|
|
168
|
-
};
|
|
169
|
-
}),
|
|
170
|
-
takeSnapshot: () => run(async () => ({ result: await target.snapshot(), patch: {} })),
|
|
171
|
-
restoreSnapshot: ({ snapshot }) => run(async () => {
|
|
172
|
-
const entry = await history.restore({ snapshot });
|
|
173
|
-
// A restore that already matched records no entry and changes nothing to mirror.
|
|
174
|
-
return { result: entry, patch: entry ? await restoredPatch(entry) : {} };
|
|
175
|
-
}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
1
|
+
/** Compatibility path; the framework-free session is shared with the React integration. */
|
|
2
|
+
export * from "./core/session.js";
|
|
178
3
|
export * from "./tools.js";
|
|
179
4
|
//# sourceMappingURL=session.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/vibecoding",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15-rc.1",
|
|
4
4
|
"description": "Conversational artifact authoring: a target-agnostic edit loop — addressable parts, validated replace, operation-level undo/redo, and per-turn snapshot/rewind. Framework-free in `/core`; tagged-region HTML documents behind ./html; filesystem targets behind ./node and the chat/preview surface behind ./react.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -87,11 +87,11 @@
|
|
|
87
87
|
"typescript": "^5.9.2",
|
|
88
88
|
"vitest": "^3.2.4",
|
|
89
89
|
"parse5": "^8.0.1",
|
|
90
|
-
"@jini-ai/core": "0.6.
|
|
90
|
+
"@jini-ai/core": "0.6.15-rc.1",
|
|
91
|
+
"@jini-ai/platform": "^0.6.15-rc.1"
|
|
91
92
|
},
|
|
92
93
|
"dependencies": {
|
|
93
|
-
"@jini-ai/
|
|
94
|
-
"@jini-ai/core": "^0.6.11"
|
|
94
|
+
"@jini-ai/core": "^0.6.15-rc.1"
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
97
|
"react": "^18.3.0 || ^19.0.0",
|