deepspace 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cli.js +353 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +124 -1
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/worker.d.ts +146 -1
- package/dist/worker.js +105 -0
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/src/ChatPanel.tsx +56 -18
- package/package.json +1 -1
- package/dist/testing.d.ts +0 -115
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
memo,
|
|
10
11
|
useState,
|
|
11
12
|
useRef,
|
|
12
13
|
useEffect,
|
|
@@ -179,6 +180,12 @@ function useStreamingChat(
|
|
|
179
180
|
if (isLoadingRef.current) return
|
|
180
181
|
setError(null)
|
|
181
182
|
lastSendRef.current = { content }
|
|
183
|
+
// Claim the in-flight slot BEFORE any await. The auto-create below is a
|
|
184
|
+
// real network round-trip; setting the guard only after it would let a
|
|
185
|
+
// second send() slip through with chatId still null and create a second
|
|
186
|
+
// chat (double-create race, orphaning the first turn).
|
|
187
|
+
isLoadingRef.current = true
|
|
188
|
+
setIsLoading(true)
|
|
182
189
|
|
|
183
190
|
let activeChatId = chatId
|
|
184
191
|
if (!activeChatId) {
|
|
@@ -198,6 +205,8 @@ function useStreamingChat(
|
|
|
198
205
|
onChatCreated?.(activeChatId)
|
|
199
206
|
} catch (err) {
|
|
200
207
|
setError(err instanceof Error ? err : new Error(String(err)))
|
|
208
|
+
isLoadingRef.current = false
|
|
209
|
+
setIsLoading(false)
|
|
201
210
|
return
|
|
202
211
|
}
|
|
203
212
|
}
|
|
@@ -209,8 +218,6 @@ function useStreamingChat(
|
|
|
209
218
|
const userMsg: InFlightMessage = { id: userMessageId, role: 'user', content, parts: [], forChatId: activeChatId }
|
|
210
219
|
const asstMsg: InFlightMessage = { id: assistantId, role: 'assistant', content: '', parts: [], forChatId: activeChatId }
|
|
211
220
|
setInFlight([userMsg, asstMsg])
|
|
212
|
-
isLoadingRef.current = true
|
|
213
|
-
setIsLoading(true)
|
|
214
221
|
abortRef.current = new AbortController()
|
|
215
222
|
|
|
216
223
|
try {
|
|
@@ -506,15 +513,24 @@ export function ChatPanel({
|
|
|
506
513
|
orderDir: 'asc',
|
|
507
514
|
})
|
|
508
515
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
516
|
+
// Persisted rows change only when the WS query delivers; mapping them (and
|
|
517
|
+
// building the id Set) in their own memos keeps that O(history) work out of
|
|
518
|
+
// the per-token recompute below AND keeps row object identities stable
|
|
519
|
+
// across streamed chunks, which is what lets memo(MessageTurn) skip
|
|
520
|
+
// re-rendering (and re-parsing markdown for) every finished turn.
|
|
521
|
+
const persisted: RenderMessage[] = useMemo(
|
|
522
|
+
() =>
|
|
523
|
+
persistedRecords.map((r) => ({
|
|
524
|
+
id: r.recordId,
|
|
525
|
+
role: r.data.role,
|
|
526
|
+
content: r.data.content ?? '',
|
|
527
|
+
parts: r.data.parts,
|
|
528
|
+
})),
|
|
529
|
+
[persistedRecords],
|
|
530
|
+
)
|
|
531
|
+
const persistedIds = useMemo(() => new Set(persisted.map((m) => m.id)), [persisted])
|
|
517
532
|
|
|
533
|
+
const messages: RenderMessage[] = useMemo(() => {
|
|
518
534
|
const tail = inFlight.filter((m) => {
|
|
519
535
|
// Only render in-flight items that belong to the chat we're currently
|
|
520
536
|
// viewing. Prevents the previous chat's bubbles from leaking through
|
|
@@ -530,7 +546,7 @@ export function ChatPanel({
|
|
|
530
546
|
return true
|
|
531
547
|
})
|
|
532
548
|
return [...persisted, ...tail]
|
|
533
|
-
}, [
|
|
549
|
+
}, [persisted, persistedIds, inFlight, chatId])
|
|
534
550
|
|
|
535
551
|
const [input, setInput] = useState('')
|
|
536
552
|
|
|
@@ -550,8 +566,12 @@ export function ChatPanel({
|
|
|
550
566
|
useEffect(() => {
|
|
551
567
|
const el = scrollRef.current
|
|
552
568
|
if (!el || !stickToBottomRef.current) return
|
|
553
|
-
|
|
554
|
-
|
|
569
|
+
// While streaming, chunks arrive faster than a smooth scroll completes —
|
|
570
|
+
// each restart leaves the view lagging the newest text and pays an extra
|
|
571
|
+
// animation. Jump instantly during the stream; ease only for discrete
|
|
572
|
+
// changes (history load, completed turn).
|
|
573
|
+
el.scrollTo({ top: el.scrollHeight, behavior: isLoading ? 'auto' : 'smooth' })
|
|
574
|
+
}, [messages, isLoading])
|
|
555
575
|
|
|
556
576
|
// Auto-grow textarea, capped at 200px.
|
|
557
577
|
useEffect(() => {
|
|
@@ -578,6 +598,10 @@ export function ChatPanel({
|
|
|
578
598
|
}
|
|
579
599
|
|
|
580
600
|
function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
|
|
601
|
+
// IME composition: the Enter that COMMITS a CJK composition fires keydown
|
|
602
|
+
// with isComposing=true (keyCode 229 on older WebKit) — it must confirm
|
|
603
|
+
// the text, not send the half-composed message.
|
|
604
|
+
if (e.nativeEvent.isComposing || e.keyCode === 229) return
|
|
581
605
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
582
606
|
e.preventDefault()
|
|
583
607
|
submit()
|
|
@@ -728,7 +752,12 @@ interface MessageTurnProps {
|
|
|
728
752
|
isStreaming: boolean
|
|
729
753
|
}
|
|
730
754
|
|
|
731
|
-
|
|
755
|
+
// memo: every streamed chunk and composer keystroke re-renders ChatPanel and
|
|
756
|
+
// with it the whole turn list. Finished turns receive referentially stable
|
|
757
|
+
// props (see the `persisted` memo), so memo lets them skip the re-render —
|
|
758
|
+
// and with it a full ReactMarkdown re-parse per turn per chunk. Only the
|
|
759
|
+
// actively streaming row's props change, so only it re-renders.
|
|
760
|
+
const MessageTurn = memo(function MessageTurn({ role, content, parts, isStreaming }: MessageTurnProps) {
|
|
732
761
|
const orderedParts: MessagePart[] = useMemo(() => {
|
|
733
762
|
if (Array.isArray(parts) && parts.length > 0) {
|
|
734
763
|
const cast = parts as MessagePart[]
|
|
@@ -804,9 +833,9 @@ function MessageTurn({ role, content, parts, isStreaming }: MessageTurnProps) {
|
|
|
804
833
|
[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1"
|
|
805
834
|
>
|
|
806
835
|
<ReactMarkdown
|
|
807
|
-
remarkPlugins={
|
|
808
|
-
rehypePlugins={
|
|
809
|
-
components={
|
|
836
|
+
remarkPlugins={REMARK_PLUGINS}
|
|
837
|
+
rehypePlugins={REHYPE_PLUGINS}
|
|
838
|
+
components={MARKDOWN_COMPONENTS}
|
|
810
839
|
>
|
|
811
840
|
{text}
|
|
812
841
|
</ReactMarkdown>
|
|
@@ -822,7 +851,7 @@ function MessageTurn({ role, content, parts, isStreaming }: MessageTurnProps) {
|
|
|
822
851
|
{isStreaming && <LiveIndicator />}
|
|
823
852
|
</div>
|
|
824
853
|
)
|
|
825
|
-
}
|
|
854
|
+
})
|
|
826
855
|
|
|
827
856
|
// ----- Empty state --------------------------------------------------------
|
|
828
857
|
|
|
@@ -943,6 +972,15 @@ const ExternalLink: Components['a'] = ({ children, href, node: _node, ...props }
|
|
|
943
972
|
)
|
|
944
973
|
}
|
|
945
974
|
|
|
975
|
+
// Module-level so ReactMarkdown's props stay referentially stable across
|
|
976
|
+
// renders — inline literals would defeat memo(MessageTurn) and re-run the
|
|
977
|
+
// remark/rehype parse for every finished turn on every streamed chunk.
|
|
978
|
+
// (Defined after CodeBlock/ExternalLink: they are const components, so an
|
|
979
|
+
// earlier literal would hit their temporal dead zone at module init.)
|
|
980
|
+
const REMARK_PLUGINS = [remarkGfm, remarkBreaks]
|
|
981
|
+
const REHYPE_PLUGINS = [rehypeHighlight]
|
|
982
|
+
const MARKDOWN_COMPONENTS = { pre: CodeBlock, a: ExternalLink }
|
|
983
|
+
|
|
946
984
|
function ToolRow({ inv }: { inv: ToolInvocation }) {
|
|
947
985
|
const done = inv.state === 'result'
|
|
948
986
|
const failed = done && inv.result?.success === false
|
package/package.json
CHANGED
package/dist/testing.d.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import * as _playwright_test from '@playwright/test';
|
|
2
|
-
import { BrowserContext, Page, Browser } from '@playwright/test';
|
|
3
|
-
export { expect } from '@playwright/test';
|
|
4
|
-
|
|
5
|
-
interface MultiplayerUser {
|
|
6
|
-
context: BrowserContext;
|
|
7
|
-
page: Page;
|
|
8
|
-
email: string;
|
|
9
|
-
name: string;
|
|
10
|
-
/** Test account user ID, if known from the accounts registry. */
|
|
11
|
-
userId?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Fixture function signature. Pass either a count (`users(2)`) or an
|
|
15
|
-
* array of account names (`users(['Alice', 'Bob'])`). Returns one entry
|
|
16
|
-
* per requested user.
|
|
17
|
-
*/
|
|
18
|
-
type UsersFixture = (selector: number | string[], options?: {
|
|
19
|
-
label?: string;
|
|
20
|
-
}) => Promise<MultiplayerUser[]>;
|
|
21
|
-
interface MultiplayerFixtures {
|
|
22
|
-
users: UsersFixture;
|
|
23
|
-
}
|
|
24
|
-
declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & MultiplayerFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Account-credential plumbing for the testing module.
|
|
28
|
-
*
|
|
29
|
-
* Reads test accounts from `~/.deepspace/test-accounts.json` (the same
|
|
30
|
-
* file `deepspace test-accounts create` writes). Each account has at
|
|
31
|
-
* minimum `{email, password}`; `name` and `label` are optional.
|
|
32
|
-
*
|
|
33
|
-
* Used by the multi-user Playwright fixture in `./fixtures.ts` and is
|
|
34
|
-
* also exported standalone in case suites want to do their own
|
|
35
|
-
* orchestration.
|
|
36
|
-
*/
|
|
37
|
-
interface TestAccount {
|
|
38
|
-
email: string;
|
|
39
|
-
password: string;
|
|
40
|
-
name?: string;
|
|
41
|
-
label?: string | null;
|
|
42
|
-
id?: string;
|
|
43
|
-
userId?: string;
|
|
44
|
-
createdAt?: number;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Load all test accounts the developer has created. Returns an empty
|
|
48
|
-
* array if the file doesn't exist yet.
|
|
49
|
-
*/
|
|
50
|
-
declare function loadAllTestAccounts(): TestAccount[];
|
|
51
|
-
/**
|
|
52
|
-
* Pick `count` test accounts from the local registry, optionally
|
|
53
|
-
* filtered by `label`. Order-stable: sorted by `createdAt` ascending so
|
|
54
|
-
* the first test account you created is always the first one returned.
|
|
55
|
-
*
|
|
56
|
-
* Throws with a helpful message if not enough accounts exist.
|
|
57
|
-
*/
|
|
58
|
-
declare function pickTestAccounts(count: number, options?: {
|
|
59
|
-
label?: string;
|
|
60
|
-
}): TestAccount[];
|
|
61
|
-
/**
|
|
62
|
-
* Find a single account by name. Used by `users(['Alice', 'Bob'])`.
|
|
63
|
-
* Throws if not found.
|
|
64
|
-
*/
|
|
65
|
-
declare function findTestAccountByName(name: string): TestAccount;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Per-account `storageState` cache for Playwright.
|
|
69
|
-
*
|
|
70
|
-
* Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`
|
|
71
|
-
* (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up
|
|
72
|
-
* 5+ users per spec would hit the limit and start failing in non-obvious
|
|
73
|
-
* ways. This module signs each account in *once*, persists the
|
|
74
|
-
* resulting browser cookies + storage to disk, and reuses the file on
|
|
75
|
-
* subsequent runs.
|
|
76
|
-
*
|
|
77
|
-
* Cache layout:
|
|
78
|
-
* ~/.deepspace/playwright-states/<sha256(email)>.json
|
|
79
|
-
*
|
|
80
|
-
* Validity: a cached file is reused if Playwright successfully loads it
|
|
81
|
-
* AND the resulting context produces a non-anonymous session on the
|
|
82
|
-
* target app. If validation fails, we fall back to a fresh sign-in and
|
|
83
|
-
* overwrite the cache.
|
|
84
|
-
*/
|
|
85
|
-
|
|
86
|
-
interface EnsureStorageStateOptions {
|
|
87
|
-
/** Max age of a cached state file before we re-sign-in. Default 7 days. */
|
|
88
|
-
maxAgeMs?: number;
|
|
89
|
-
/** Force a fresh sign-in even if the cache is fresh. */
|
|
90
|
-
force?: boolean;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Ensure a Playwright `storageState` file exists for `account` and
|
|
94
|
-
* return its path. Signs in once if the cache is missing, stale, or
|
|
95
|
-
* `force: true`.
|
|
96
|
-
*/
|
|
97
|
-
declare function ensureStorageState(browser: Browser, account: {
|
|
98
|
-
email: string;
|
|
99
|
-
password: string;
|
|
100
|
-
}, baseURL: string, options?: EnsureStorageStateOptions): Promise<string>;
|
|
101
|
-
/**
|
|
102
|
-
* Convenience: open a fresh `BrowserContext` for `account` using the
|
|
103
|
-
* cached storage state (signing in if needed). The caller is responsible
|
|
104
|
-
* for closing the context.
|
|
105
|
-
*/
|
|
106
|
-
declare function newSignedInContext(browser: Browser, account: {
|
|
107
|
-
email: string;
|
|
108
|
-
password: string;
|
|
109
|
-
}, baseURL: string, options?: EnsureStorageStateOptions): Promise<BrowserContext>;
|
|
110
|
-
/** Read a cached storageState file (mostly for debugging). */
|
|
111
|
-
declare function readCachedState(email: string): unknown | null;
|
|
112
|
-
/** Where the cache for `email` lives. Useful for diagnostics. */
|
|
113
|
-
declare function getStatePathForEmail(email: string): string;
|
|
114
|
-
|
|
115
|
-
export { type EnsureStorageStateOptions, type MultiplayerUser, type TestAccount, type UsersFixture, ensureStorageState, findTestAccountByName, getStatePathForEmail, loadAllTestAccounts, newSignedInContext, pickTestAccounts, readCachedState, test };
|