@stigmer/react 3.1.4 → 3.1.6
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/execution/FileReviewContext.d.ts +15 -1
- package/execution/FileReviewContext.d.ts.map +1 -1
- package/execution/FileReviewContext.js +20 -1
- package/execution/FileReviewContext.js.map +1 -1
- package/execution/MessageThread.d.ts +65 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +42 -23
- package/execution/MessageThread.js.map +1 -1
- package/execution/TodoCard.d.ts +8 -0
- package/execution/TodoCard.d.ts.map +1 -1
- package/execution/TodoCard.js +6 -4
- package/execution/TodoCard.js.map +1 -1
- package/execution/TodoList.d.ts +20 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +9 -6
- package/execution/TodoList.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +3 -2
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +49 -16
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/WriteBackCard.d.ts +7 -1
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +10 -2
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/file-review-status.d.ts +15 -0
- package/execution/file-review-status.d.ts.map +1 -1
- package/execution/file-review-status.js +22 -0
- package/execution/file-review-status.js.map +1 -1
- package/execution/index.d.ts +5 -5
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +3 -3
- package/execution/index.js.map +1 -1
- package/github/useGitHubFileReader.d.ts +4 -2
- package/github/useGitHubFileReader.d.ts.map +1 -1
- package/github/useGitHubFileReader.js +16 -3
- package/github/useGitHubFileReader.js.map +1 -1
- package/github/useGitHubTreeLister.d.ts +2 -1
- package/github/useGitHubTreeLister.d.ts.map +1 -1
- package/github/useGitHubTreeLister.js +7 -3
- package/github/useGitHubTreeLister.js.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/VirtualizedThread.d.ts +3 -2
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/package.json +4 -4
- package/session/SessionViewer.d.ts +11 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +24 -5
- package/session/SessionViewer.js.map +1 -1
- package/session/facets/ChangesTab.d.ts +24 -6
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +15 -6
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/index.d.ts +1 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +1 -0
- package/session/index.js.map +1 -1
- package/session/useSessionRailViews.d.ts +16 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +9 -4
- package/session/useSessionRailViews.js.map +1 -1
- package/session/useWorkspaceReadRefs.d.ts +45 -0
- package/session/useWorkspaceReadRefs.d.ts.map +1 -0
- package/session/useWorkspaceReadRefs.js +104 -0
- package/session/useWorkspaceReadRefs.js.map +1 -0
- package/src/execution/FileReviewContext.ts +27 -1
- package/src/execution/MessageThread.tsx +110 -22
- package/src/execution/TodoCard.tsx +20 -4
- package/src/execution/TodoList.tsx +26 -4
- package/src/execution/ToolCallDetail.tsx +69 -16
- package/src/execution/WriteBackCard.tsx +18 -4
- package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
- package/src/execution/__tests__/WriteBackCard.test.tsx +83 -0
- package/src/execution/__tests__/file-review-status.test.ts +47 -0
- package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
- package/src/execution/file-review-status.ts +24 -0
- package/src/execution/index.ts +13 -3
- package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
- package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
- package/src/github/useGitHubFileReader.ts +19 -6
- package/src/github/useGitHubTreeLister.ts +7 -3
- package/src/index.ts +4 -0
- package/src/internal/VirtualizedThread.tsx +5 -1
- package/src/session/SessionViewer.tsx +42 -2
- package/src/session/__tests__/useSessionRailViews.test.tsx +12 -0
- package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
- package/src/session/facets/ChangesTab.tsx +43 -6
- package/src/session/facets/__tests__/ChangesTab.test.tsx +23 -0
- package/src/session/index.ts +2 -0
- package/src/session/useSessionRailViews.tsx +31 -3
- package/src/session/useWorkspaceReadRefs.ts +118 -0
- package/src/workflow/picker/__tests__/recents.test.ts +4 -4
- package/src/workspace/FileViewer.tsx +144 -38
- package/src/workspace/WorkspaceFileReader.ts +21 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
- package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
- package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
- package/src/workspace/index.ts +4 -1
- package/src/workspace/useWorkspaceEntries.ts +11 -0
- package/src/workspace/useWorkspaceFileContent.ts +4 -1
- package/src/workspace/useWorkspaceFileSearch.ts +8 -3
- package/src/workspace/useWorkspaceFiles.ts +3 -2
- package/src/workspace/workspaceListingCache.ts +21 -7
- package/styles.css +1 -1
- package/workspace/FileViewer.d.ts +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +55 -5
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceFileReader.d.ts +17 -0
- package/workspace/WorkspaceFileReader.d.ts.map +1 -1
- package/workspace/WorkspaceFileReader.js +18 -0
- package/workspace/WorkspaceFileReader.js.map +1 -1
- package/workspace/index.d.ts +1 -1
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -1
- package/workspace/index.js.map +1 -1
- package/workspace/useWorkspaceEntries.d.ts +11 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js.map +1 -1
- package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
- package/workspace/useWorkspaceFileContent.js +4 -1
- package/workspace/useWorkspaceFileContent.js.map +1 -1
- package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/useWorkspaceFileSearch.js +8 -3
- package/workspace/useWorkspaceFileSearch.js.map +1 -1
- package/workspace/useWorkspaceFiles.d.ts +2 -1
- package/workspace/useWorkspaceFiles.d.ts.map +1 -1
- package/workspace/useWorkspaceFiles.js +3 -2
- package/workspace/useWorkspaceFiles.js.map +1 -1
- package/workspace/workspaceListingCache.d.ts +1 -1
- package/workspace/workspaceListingCache.d.ts.map +1 -1
- package/workspace/workspaceListingCache.js +20 -7
- package/workspace/workspaceListingCache.js.map +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { WorkspaceEntry } from "../useWorkspaceEntries";
|
|
3
|
+
import type { WorkspaceFileEntry, WorkspaceFileLister } from "../WorkspaceFileLister";
|
|
4
|
+
import {
|
|
5
|
+
loadEntryFiles,
|
|
6
|
+
peekEntryListing,
|
|
7
|
+
__clearWorkspaceListingCache,
|
|
8
|
+
} from "../workspaceListingCache";
|
|
9
|
+
|
|
10
|
+
function gitEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
|
|
11
|
+
return {
|
|
12
|
+
id: "ws-1",
|
|
13
|
+
name: "acme/api",
|
|
14
|
+
type: "git",
|
|
15
|
+
gitUrl: "https://github.com/acme/api",
|
|
16
|
+
gitBranch: "main",
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function listerFor(files: readonly WorkspaceFileEntry[]): WorkspaceFileLister {
|
|
22
|
+
return vi.fn(async () => [...files]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("workspaceListingCache", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
__clearWorkspaceListingCache();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("caches a listing and serves it without re-calling the lister", async () => {
|
|
31
|
+
const lister = listerFor([{ path: "a.ts", isDirectory: false }]);
|
|
32
|
+
const entry = gitEntry();
|
|
33
|
+
|
|
34
|
+
await loadEntryFiles(entry, lister);
|
|
35
|
+
await loadEntryFiles(entry, lister);
|
|
36
|
+
|
|
37
|
+
expect(lister).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(peekEntryListing(entry)?.files).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("treats a readRef advance as a different listing (stale-tree regression)", async () => {
|
|
42
|
+
// The write-back pushed a commit that added notes.md; the pre-push listing
|
|
43
|
+
// must not be served for the post-push ref.
|
|
44
|
+
const prePush = gitEntry(); // reads at gitBranch
|
|
45
|
+
const postPush = gitEntry({ readRef: "sha-after-push" });
|
|
46
|
+
|
|
47
|
+
const lister = vi.fn(async (entry: WorkspaceEntry) =>
|
|
48
|
+
entry.readRef
|
|
49
|
+
? [
|
|
50
|
+
{ path: "a.ts", isDirectory: false },
|
|
51
|
+
{ path: "notes.md", isDirectory: false },
|
|
52
|
+
]
|
|
53
|
+
: [{ path: "a.ts", isDirectory: false }],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const before = await loadEntryFiles(prePush, lister);
|
|
57
|
+
expect(before?.files.map((f) => f.path)).toEqual(["a.ts"]);
|
|
58
|
+
|
|
59
|
+
// Same entry.id, advanced ref — must re-fetch, not serve the stale tree.
|
|
60
|
+
const after = await loadEntryFiles(postPush, lister);
|
|
61
|
+
expect(lister).toHaveBeenCalledTimes(2);
|
|
62
|
+
expect(after?.files.map((f) => f.path)).toContain("notes.md");
|
|
63
|
+
|
|
64
|
+
// Both snapshots remain independently peekable at their own refs.
|
|
65
|
+
expect(peekEntryListing(prePush)?.files).toHaveLength(1);
|
|
66
|
+
expect(peekEntryListing(postPush)?.files).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("keys by entry id as well — two entries at the same ref never collide", async () => {
|
|
70
|
+
const one = gitEntry({ id: "ws-1" });
|
|
71
|
+
const two = gitEntry({ id: "ws-2", name: "acme/web", gitUrl: "https://github.com/acme/web" });
|
|
72
|
+
|
|
73
|
+
await loadEntryFiles(one, listerFor([{ path: "one.ts", isDirectory: false }]));
|
|
74
|
+
await loadEntryFiles(two, listerFor([{ path: "two.ts", isDirectory: false }]));
|
|
75
|
+
|
|
76
|
+
expect(peekEntryListing(one)?.files[0].path).toBe("one.ts");
|
|
77
|
+
expect(peekEntryListing(two)?.files[0].path).toBe("two.ts");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("bustCache re-calls the lister for the same entry and ref", async () => {
|
|
81
|
+
const lister = listerFor([{ path: "a.ts", isDirectory: false }]);
|
|
82
|
+
const entry = gitEntry();
|
|
83
|
+
|
|
84
|
+
await loadEntryFiles(entry, lister);
|
|
85
|
+
await loadEntryFiles(entry, lister, { bustCache: true });
|
|
86
|
+
|
|
87
|
+
expect(lister).toHaveBeenCalledTimes(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("never caches a null (unsupported) result", async () => {
|
|
91
|
+
const lister: WorkspaceFileLister = vi.fn(async () => null);
|
|
92
|
+
const entry = gitEntry();
|
|
93
|
+
|
|
94
|
+
expect(await loadEntryFiles(entry, lister)).toBeNull();
|
|
95
|
+
expect(peekEntryListing(entry)).toBeUndefined();
|
|
96
|
+
|
|
97
|
+
// A later, supporting lister must get its chance.
|
|
98
|
+
await loadEntryFiles(entry, listerFor([{ path: "a.ts", isDirectory: false }]));
|
|
99
|
+
expect(peekEntryListing(entry)?.files).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("collapses advisory notice entries into the truncated flag", async () => {
|
|
103
|
+
const lister = listerFor([
|
|
104
|
+
{ path: "a.ts", isDirectory: false },
|
|
105
|
+
{ path: "... (tree truncated)", isDirectory: false, notice: true },
|
|
106
|
+
]);
|
|
107
|
+
const entry = gitEntry();
|
|
108
|
+
|
|
109
|
+
const listing = await loadEntryFiles(entry, lister);
|
|
110
|
+
|
|
111
|
+
expect(listing?.truncated).toBe(true);
|
|
112
|
+
expect(listing?.files.map((f) => f.path)).toEqual(["a.ts"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/workspace/index.ts
CHANGED
|
@@ -7,7 +7,10 @@ export type {
|
|
|
7
7
|
WorkspaceFileEntry,
|
|
8
8
|
WorkspaceFileLister,
|
|
9
9
|
} from "./WorkspaceFileLister.js";
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
MAX_WORKSPACE_FILE_READ_BYTES,
|
|
12
|
+
WorkspaceFileNotFoundError,
|
|
13
|
+
} from "./WorkspaceFileReader.js";
|
|
11
14
|
export type {
|
|
12
15
|
WorkspaceFileContent,
|
|
13
16
|
WorkspaceFileReader,
|
|
@@ -33,6 +33,17 @@ export interface WorkspaceEntry {
|
|
|
33
33
|
readonly gitUrl?: string;
|
|
34
34
|
/** Branch to clone. Set when `type` is `"git"`. */
|
|
35
35
|
readonly gitBranch?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The git ref (branch or commit SHA) at which read-side operations —
|
|
38
|
+
* file content, tree listing, name search — resolve. Defaults to
|
|
39
|
+
* `gitBranch` when absent.
|
|
40
|
+
*
|
|
41
|
+
* Never user-configured: a session derives it from the latest workspace
|
|
42
|
+
* write-back so viewers read the agent's pushed commit instead of the
|
|
43
|
+
* stale base branch (see `useWorkspaceReadRefs`). A read-side projection
|
|
44
|
+
* only — never persisted, never part of the session input.
|
|
45
|
+
*/
|
|
46
|
+
readonly readRef?: string;
|
|
36
47
|
/** Absolute filesystem path. Set when `type` is `"local"`. */
|
|
37
48
|
readonly localPath?: string;
|
|
38
49
|
}
|
|
@@ -105,7 +105,10 @@ export function useWorkspaceFileContent({
|
|
|
105
105
|
? () => reader(entry, path).then((c) => ({ content: c }))
|
|
106
106
|
: null,
|
|
107
107
|
// Identity deps: a change to any of these is a different file to read.
|
|
108
|
-
|
|
108
|
+
// The read ref participates because the same path at a different ref is
|
|
109
|
+
// different content — a write-back push advances `readRef` while
|
|
110
|
+
// `entry.id` stays stable.
|
|
111
|
+
[entry?.id ?? null, entry?.readRef ?? null, path ?? null, reader],
|
|
109
112
|
EMPTY_CONTENT,
|
|
110
113
|
);
|
|
111
114
|
|
|
@@ -82,11 +82,16 @@ export function useWorkspaceFileSearch({
|
|
|
82
82
|
}: UseWorkspaceFileSearchOptions): UseWorkspaceFileSearchReturn {
|
|
83
83
|
const [states, setStates] = useState<Record<string, EntryListingState>>({});
|
|
84
84
|
|
|
85
|
-
// Latest entries read inside the effect without widening its dependency to
|
|
86
|
-
// (possibly per-render) array identity — the effect keys on the
|
|
85
|
+
// Latest entries read inside the effect without widening its dependency to
|
|
86
|
+
// the (possibly per-render) array identity — the effect keys on the set of
|
|
87
|
+
// (id, readRef) pairs instead. The ref participates because a listing is a
|
|
88
|
+
// snapshot of one ref: when a write-back advances an entry's readRef, its
|
|
89
|
+
// listing must be re-fetched even though the id set is unchanged.
|
|
87
90
|
const entriesRef = useRef(entries);
|
|
88
91
|
entriesRef.current = entries;
|
|
89
|
-
const entriesKey = entries
|
|
92
|
+
const entriesKey = entries
|
|
93
|
+
.map((entry) => `${entry.id}\u0001${entry.readRef ?? ""}`)
|
|
94
|
+
.join("\u0000");
|
|
90
95
|
|
|
91
96
|
// Ignore resolutions from a superseded entry set (id set or lister changed).
|
|
92
97
|
const generationRef = useRef(0);
|
|
@@ -43,7 +43,8 @@ const EMPTY_TREE: readonly TreeNode[] = [];
|
|
|
43
43
|
* {@link loadEntryFiles} cache) when the
|
|
44
44
|
* entry changes.
|
|
45
45
|
* - Reads the shared listing cache so tab switches, re-expands, and the search
|
|
46
|
-
* surface are instant without re-fetching (one cache, keyed by `entry.id`
|
|
46
|
+
* surface are instant without re-fetching (one cache, keyed by `entry.id` +
|
|
47
|
+
* effective read ref — a ref advance is a cache miss by design).
|
|
47
48
|
* - Returns an empty tree and skips the call when `lister` is
|
|
48
49
|
* `undefined` (graceful degradation — DD-011 opt-in).
|
|
49
50
|
* - Memoizes the return value for referential stability (DD-010).
|
|
@@ -72,7 +73,7 @@ export function useWorkspaceFiles({
|
|
|
72
73
|
if (!lister) return;
|
|
73
74
|
|
|
74
75
|
if (!bustCache) {
|
|
75
|
-
const cached = peekEntryListing(target
|
|
76
|
+
const cached = peekEntryListing(target);
|
|
76
77
|
if (cached) {
|
|
77
78
|
setTree(cached.tree);
|
|
78
79
|
setTruncated(cached.truncated);
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// Both surfaces that need a listing read through here: the Files accordion
|
|
4
4
|
// (`useWorkspaceFiles`, one entry) and workspace search (`useWorkspaceFileSearch`,
|
|
5
5
|
// many entries). Keeping one module-level cache — keyed by the stable-within-page
|
|
6
|
-
// `entry.id`
|
|
7
|
-
//
|
|
8
|
-
// accordion, and the truncation flag for
|
|
6
|
+
// `entry.id` plus the entry's effective read ref — guarantees the two never
|
|
7
|
+
// double-fetch or drift, and lets one fetch produce every shape a consumer needs
|
|
8
|
+
// (flat files for search, a tree for the accordion, and the truncation flag for
|
|
9
|
+
// the banner). The ref is part of the key because a listing is a snapshot of one
|
|
10
|
+
// ref: when a session's write-back advances `readRef`, the old listing is stale
|
|
11
|
+
// by definition and must not be served.
|
|
9
12
|
|
|
10
13
|
import { buildFileTree, type TreeNode } from "../internal/file-tree/index.js";
|
|
11
14
|
import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
|
|
@@ -40,13 +43,24 @@ export interface LoadEntryFilesOptions {
|
|
|
40
43
|
|
|
41
44
|
const sharedCache = new Map<string, WorkspaceListing>();
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Cache key for one entry's listing: identity plus the ref the listing was
|
|
48
|
+
* taken at. `entry.id` alone is NOT sufficient — it anchors file-selection
|
|
49
|
+
* identity across the surface and stays stable while `readRef` advances with
|
|
50
|
+
* each write-back push, so the ref must contribute to the key or consumers
|
|
51
|
+
* would see the pre-push tree forever.
|
|
52
|
+
*/
|
|
53
|
+
function listingCacheKey(entry: WorkspaceEntry): string {
|
|
54
|
+
return `${entry.id}\u0000${entry.readRef ?? entry.gitBranch ?? ""}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
58
|
* Returns a cached {@link WorkspaceListing} for an entry synchronously, or
|
|
45
59
|
* `undefined` when nothing is cached. Lets a consumer render a warm listing
|
|
46
60
|
* instantly (no loading flash) before deciding whether to fetch.
|
|
47
61
|
*/
|
|
48
|
-
export function peekEntryListing(
|
|
49
|
-
return sharedCache.get(
|
|
62
|
+
export function peekEntryListing(entry: WorkspaceEntry): WorkspaceListing | undefined {
|
|
63
|
+
return sharedCache.get(listingCacheKey(entry));
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
@@ -63,7 +77,7 @@ export async function loadEntryFiles(
|
|
|
63
77
|
options: LoadEntryFilesOptions = {},
|
|
64
78
|
): Promise<WorkspaceListing | null> {
|
|
65
79
|
if (!options.bustCache) {
|
|
66
|
-
const cached = sharedCache.get(entry
|
|
80
|
+
const cached = sharedCache.get(listingCacheKey(entry));
|
|
67
81
|
if (cached) return cached;
|
|
68
82
|
}
|
|
69
83
|
|
|
@@ -71,7 +85,7 @@ export async function loadEntryFiles(
|
|
|
71
85
|
if (raw === null) return null;
|
|
72
86
|
|
|
73
87
|
const listing = buildListing(raw);
|
|
74
|
-
sharedCache.set(entry
|
|
88
|
+
sharedCache.set(listingCacheKey(entry), listing);
|
|
75
89
|
return listing;
|
|
76
90
|
}
|
|
77
91
|
|