deepspace 0.6.1 → 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 +31 -0
- package/dist/cli.js +446 -92
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +126 -1
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +53 -1
- package/dist/server.js +86 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +199 -2
- package/dist/worker.js +191 -1
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/src/ChatPanel.tsx +70 -30
- 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,
|
|
@@ -105,19 +106,21 @@ const DEFAULT_PROMPTS = [
|
|
|
105
106
|
'List my collections',
|
|
106
107
|
]
|
|
107
108
|
|
|
108
|
-
// Mirrors the allowlist in the scaffolded
|
|
109
|
-
// default — balanced cost/capability with the same 1M-token context as
|
|
110
|
-
//
|
|
111
|
-
// so the picker shows the
|
|
112
|
-
// Override by passing your own `models` prop
|
|
109
|
+
// Mirrors the allowlist in the scaffolded ai/chat-routes.ts. Sonnet 5 is the
|
|
110
|
+
// default — balanced cost/capability with the same 1M-token context as Opus
|
|
111
|
+
// at a lower price. Within each provider we list default → premium → cheap
|
|
112
|
+
// so the picker shows the recommended option first when a user opens it.
|
|
113
|
+
// Override by passing your own `models` prop — apps scaffolded before this
|
|
114
|
+
// lineup must either update their ALLOWED_MODELS to match or pass the models
|
|
115
|
+
// they allowlist, otherwise the worker rejects the picker's ids with a 400.
|
|
113
116
|
const DEFAULT_MODELS: ModelOption[] = [
|
|
114
|
-
{ id: 'claude-sonnet-
|
|
115
|
-
{ id: 'claude-opus-4-
|
|
116
|
-
{ id: 'claude-haiku-4-5',
|
|
117
|
-
{ id: 'gpt-5.
|
|
118
|
-
{ id: 'gpt-5.
|
|
119
|
-
{ id: 'gpt-5.
|
|
120
|
-
{ id: 'gpt-oss-120b',
|
|
117
|
+
{ id: 'claude-sonnet-5', label: 'Claude Sonnet 5', provider: 'Anthropic' },
|
|
118
|
+
{ id: 'claude-opus-4-8', label: 'Claude Opus 4.8', provider: 'Anthropic' },
|
|
119
|
+
{ id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', provider: 'Anthropic' },
|
|
120
|
+
{ id: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', provider: 'OpenAI' },
|
|
121
|
+
{ id: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', provider: 'OpenAI' },
|
|
122
|
+
{ id: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', provider: 'OpenAI' },
|
|
123
|
+
{ id: 'gpt-oss-120b', label: 'GPT-OSS 120B', provider: 'Cerebras' },
|
|
121
124
|
]
|
|
122
125
|
|
|
123
126
|
// ============================================================================
|
|
@@ -177,6 +180,12 @@ function useStreamingChat(
|
|
|
177
180
|
if (isLoadingRef.current) return
|
|
178
181
|
setError(null)
|
|
179
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)
|
|
180
189
|
|
|
181
190
|
let activeChatId = chatId
|
|
182
191
|
if (!activeChatId) {
|
|
@@ -196,6 +205,8 @@ function useStreamingChat(
|
|
|
196
205
|
onChatCreated?.(activeChatId)
|
|
197
206
|
} catch (err) {
|
|
198
207
|
setError(err instanceof Error ? err : new Error(String(err)))
|
|
208
|
+
isLoadingRef.current = false
|
|
209
|
+
setIsLoading(false)
|
|
199
210
|
return
|
|
200
211
|
}
|
|
201
212
|
}
|
|
@@ -207,8 +218,6 @@ function useStreamingChat(
|
|
|
207
218
|
const userMsg: InFlightMessage = { id: userMessageId, role: 'user', content, parts: [], forChatId: activeChatId }
|
|
208
219
|
const asstMsg: InFlightMessage = { id: assistantId, role: 'assistant', content: '', parts: [], forChatId: activeChatId }
|
|
209
220
|
setInFlight([userMsg, asstMsg])
|
|
210
|
-
isLoadingRef.current = true
|
|
211
|
-
setIsLoading(true)
|
|
212
221
|
abortRef.current = new AbortController()
|
|
213
222
|
|
|
214
223
|
try {
|
|
@@ -504,15 +513,24 @@ export function ChatPanel({
|
|
|
504
513
|
orderDir: 'asc',
|
|
505
514
|
})
|
|
506
515
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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])
|
|
515
532
|
|
|
533
|
+
const messages: RenderMessage[] = useMemo(() => {
|
|
516
534
|
const tail = inFlight.filter((m) => {
|
|
517
535
|
// Only render in-flight items that belong to the chat we're currently
|
|
518
536
|
// viewing. Prevents the previous chat's bubbles from leaking through
|
|
@@ -528,7 +546,7 @@ export function ChatPanel({
|
|
|
528
546
|
return true
|
|
529
547
|
})
|
|
530
548
|
return [...persisted, ...tail]
|
|
531
|
-
}, [
|
|
549
|
+
}, [persisted, persistedIds, inFlight, chatId])
|
|
532
550
|
|
|
533
551
|
const [input, setInput] = useState('')
|
|
534
552
|
|
|
@@ -548,8 +566,12 @@ export function ChatPanel({
|
|
|
548
566
|
useEffect(() => {
|
|
549
567
|
const el = scrollRef.current
|
|
550
568
|
if (!el || !stickToBottomRef.current) return
|
|
551
|
-
|
|
552
|
-
|
|
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])
|
|
553
575
|
|
|
554
576
|
// Auto-grow textarea, capped at 200px.
|
|
555
577
|
useEffect(() => {
|
|
@@ -576,6 +598,10 @@ export function ChatPanel({
|
|
|
576
598
|
}
|
|
577
599
|
|
|
578
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
|
|
579
605
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
580
606
|
e.preventDefault()
|
|
581
607
|
submit()
|
|
@@ -726,7 +752,12 @@ interface MessageTurnProps {
|
|
|
726
752
|
isStreaming: boolean
|
|
727
753
|
}
|
|
728
754
|
|
|
729
|
-
|
|
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) {
|
|
730
761
|
const orderedParts: MessagePart[] = useMemo(() => {
|
|
731
762
|
if (Array.isArray(parts) && parts.length > 0) {
|
|
732
763
|
const cast = parts as MessagePart[]
|
|
@@ -802,9 +833,9 @@ function MessageTurn({ role, content, parts, isStreaming }: MessageTurnProps) {
|
|
|
802
833
|
[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1"
|
|
803
834
|
>
|
|
804
835
|
<ReactMarkdown
|
|
805
|
-
remarkPlugins={
|
|
806
|
-
rehypePlugins={
|
|
807
|
-
components={
|
|
836
|
+
remarkPlugins={REMARK_PLUGINS}
|
|
837
|
+
rehypePlugins={REHYPE_PLUGINS}
|
|
838
|
+
components={MARKDOWN_COMPONENTS}
|
|
808
839
|
>
|
|
809
840
|
{text}
|
|
810
841
|
</ReactMarkdown>
|
|
@@ -820,7 +851,7 @@ function MessageTurn({ role, content, parts, isStreaming }: MessageTurnProps) {
|
|
|
820
851
|
{isStreaming && <LiveIndicator />}
|
|
821
852
|
</div>
|
|
822
853
|
)
|
|
823
|
-
}
|
|
854
|
+
})
|
|
824
855
|
|
|
825
856
|
// ----- Empty state --------------------------------------------------------
|
|
826
857
|
|
|
@@ -941,6 +972,15 @@ const ExternalLink: Components['a'] = ({ children, href, node: _node, ...props }
|
|
|
941
972
|
)
|
|
942
973
|
}
|
|
943
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
|
+
|
|
944
984
|
function ToolRow({ inv }: { inv: ToolInvocation }) {
|
|
945
985
|
const done = inv.state === 'result'
|
|
946
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 };
|