@puredesktop/puredesktop-ui-bridge 2.1.7 → 2.1.8
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/package.json +55 -1
- package/src/bridge/agentTypes.ts +21 -1
- package/src/bridge/collectionImagePaste.ts +19 -1
- package/src/bridge/context.mjs +32 -0
- package/src/bridge/documents.d.mts +76 -0
- package/src/bridge/documents.mjs +40 -0
- package/src/bridge/methods.d.mts +23 -0
- package/src/bridge/methods.mjs +23 -0
- package/src/bridge/pureRender/base.css +44 -0
- package/src/bridge/pureRender/document.ts +29 -2
- package/src/bridge/pureRender/index.ts +3 -0
- package/src/bridge/pureRender/normalize.ts +167 -0
- package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
- package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
- package/src/bridge/pureRender/styles.ts +125 -5
- package/src/bridge/pureRender/theme.test.ts +259 -0
- package/src/bridge/pureRender/theme.ts +19 -1
- package/src/bridge/pureRender/types.ts +33 -0
- package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
- package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
- package/src/bridge/react/useDocumentHotkeys.ts +41 -0
- package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
- package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
- package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
- package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
- package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
- package/src/bridge/storage.d.mts +6 -6
- package/src/bridge/storage.test.ts +6 -6
- package/src/bridge/types.ts +7 -0
- package/src/components/agents/AgentDrawerPanel.tsx +2 -0
- package/src/components/agents/AgentMessageList.tsx +6 -1
- package/src/components/agents/AgentToolCallCard.tsx +20 -20
- package/src/components/agents/ContextPicker.tsx +239 -0
- package/src/components/agents/agentPanelStyles.ts +3 -2
- package/src/components/agents/agentTypes.ts +2 -0
- package/src/components/chrome/WorkspaceTabStrip.tsx +18 -1
- package/src/components/chrome/workspaceTabTypes.ts +2 -0
- package/src/components/common/containers/AppFrame.test.tsx +4 -4
- package/src/components/common/containers/AppFrame.tsx +10 -1
- package/src/components/common/containers/AppHeader.tsx +22 -0
- package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
- package/src/components/common/documents/DocumentSwitcher.tsx +1176 -0
- package/src/components/common/documents/index.ts +8 -0
- package/src/components/common/research/EvidenceDossier.tsx +1232 -150
- package/src/components/common/research/index.ts +2 -0
- package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
- package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
- package/src/components/print-design/index.ts +2 -0
- package/src/components/print-design/previewContent.test.ts +32 -0
- package/src/components/print-design/previewContent.ts +31 -0
- package/src/context/buildContextDocument.test.ts +240 -0
- package/src/context/buildContextDocument.ts +309 -0
- package/src/context/contextAccess.ts +66 -0
- package/src/context/contextConfig.ts +72 -0
- package/src/context/contextDocument.test.ts +155 -0
- package/src/context/contextDocument.ts +300 -0
- package/src/context/contextSections.test.ts +88 -0
- package/src/context/contextSections.ts +84 -0
- package/src/context/htmlToContextMarkdown.test.ts +134 -0
- package/src/context/htmlToContextMarkdown.ts +369 -0
- package/src/theme/appAccents.test.ts +36 -35
- package/src/theme/appAccents.ts +87 -104
- package/src/theme/appIdentityCss.mjs +169 -231
- package/src/theme/appIdentityCss.ts +213 -233
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
readPlatformStorageJson,
|
|
4
|
+
writePlatformStorageJson,
|
|
5
|
+
} from '../storage.mjs'
|
|
6
|
+
|
|
7
|
+
export type PlatformJsonStoreFileName = `${string}.json`
|
|
8
|
+
|
|
9
|
+
export interface UsePlatformJsonStoreOptions<TValue> {
|
|
10
|
+
appSlug: string | null
|
|
11
|
+
fileName: PlatformJsonStoreFileName | null
|
|
12
|
+
/** Default value when the JSON file does not exist yet. */
|
|
13
|
+
initialValue: TValue
|
|
14
|
+
/** Wait for bridge ready before loading (default: true when appSlug/fileName exist). */
|
|
15
|
+
enabled?: boolean
|
|
16
|
+
/** Normalize parsed JSON into the app-owned store shape. */
|
|
17
|
+
parse?: (value: unknown) => TValue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UsePlatformJsonStoreResult<TValue> {
|
|
21
|
+
value: TValue
|
|
22
|
+
path: string | null
|
|
23
|
+
loading: boolean
|
|
24
|
+
saving: boolean
|
|
25
|
+
error: Error | null
|
|
26
|
+
saveValue: (next: TValue) => Promise<TValue>
|
|
27
|
+
updateValue: (updater: (current: TValue) => TValue) => Promise<TValue>
|
|
28
|
+
reload: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultParse<TValue>(value: unknown): TValue {
|
|
32
|
+
return value as TValue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load and replace one shell-managed JSON store file through `storage.*`.
|
|
37
|
+
* Use this for app-owned internal records, not for user-visible documents.
|
|
38
|
+
*/
|
|
39
|
+
export function usePlatformJsonStore<TValue>({
|
|
40
|
+
appSlug,
|
|
41
|
+
fileName,
|
|
42
|
+
initialValue,
|
|
43
|
+
enabled = Boolean(appSlug && fileName),
|
|
44
|
+
parse = defaultParse<TValue>,
|
|
45
|
+
}: UsePlatformJsonStoreOptions<TValue>): UsePlatformJsonStoreResult<TValue> {
|
|
46
|
+
const [value, setValue] = useState<TValue>(initialValue)
|
|
47
|
+
const [path, setPath] = useState<string | null>(null)
|
|
48
|
+
const [loading, setLoading] = useState(false)
|
|
49
|
+
const [saving, setSaving] = useState(false)
|
|
50
|
+
const [error, setError] = useState<Error | null>(null)
|
|
51
|
+
const [reloadToken, setReloadToken] = useState(0)
|
|
52
|
+
const valueRef = useRef(value)
|
|
53
|
+
const writeChainRef = useRef<Promise<TValue>>(Promise.resolve(value))
|
|
54
|
+
valueRef.current = value
|
|
55
|
+
|
|
56
|
+
const reload = useCallback(() => {
|
|
57
|
+
setReloadToken(token => token + 1)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!enabled || !appSlug || !fileName) return
|
|
62
|
+
|
|
63
|
+
let cancelled = false
|
|
64
|
+
setLoading(true)
|
|
65
|
+
setError(null)
|
|
66
|
+
|
|
67
|
+
void readPlatformStorageJson({ appSlug, fileName })
|
|
68
|
+
.then(result => {
|
|
69
|
+
if (cancelled) return
|
|
70
|
+
const next =
|
|
71
|
+
result.value === null ? initialValue : parse(result.value)
|
|
72
|
+
valueRef.current = next
|
|
73
|
+
setValue(next)
|
|
74
|
+
setPath(result.path)
|
|
75
|
+
})
|
|
76
|
+
.catch((cause: unknown) => {
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.finally(() => {
|
|
82
|
+
if (!cancelled) setLoading(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
cancelled = true
|
|
87
|
+
}
|
|
88
|
+
}, [appSlug, enabled, fileName, initialValue, parse, reloadToken])
|
|
89
|
+
|
|
90
|
+
const queueSave = useCallback(
|
|
91
|
+
(resolveNext: (current: TValue) => TValue): Promise<TValue> => {
|
|
92
|
+
if (!appSlug || !fileName) {
|
|
93
|
+
throw new Error('appSlug and fileName are required to save JSON storage')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const write = async (): Promise<TValue> => {
|
|
97
|
+
const next = resolveNext(valueRef.current)
|
|
98
|
+
setSaving(true)
|
|
99
|
+
try {
|
|
100
|
+
const result = await writePlatformStorageJson({
|
|
101
|
+
appSlug,
|
|
102
|
+
fileName,
|
|
103
|
+
value: next,
|
|
104
|
+
})
|
|
105
|
+
valueRef.current = next
|
|
106
|
+
setValue(next)
|
|
107
|
+
setPath(result.path)
|
|
108
|
+
setError(null)
|
|
109
|
+
return next
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
const error =
|
|
112
|
+
cause instanceof Error ? cause : new Error(String(cause))
|
|
113
|
+
setError(error)
|
|
114
|
+
throw error
|
|
115
|
+
} finally {
|
|
116
|
+
setSaving(false)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const queued = writeChainRef.current.then(write, write)
|
|
121
|
+
writeChainRef.current = queued.catch(() => valueRef.current)
|
|
122
|
+
return queued
|
|
123
|
+
},
|
|
124
|
+
[appSlug, fileName],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const saveValue = useCallback(
|
|
128
|
+
(next: TValue): Promise<TValue> => queueSave(() => next),
|
|
129
|
+
[queueSave],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const updateValue = useCallback(
|
|
133
|
+
(updater: (current: TValue) => TValue): Promise<TValue> => {
|
|
134
|
+
return queueSave(updater)
|
|
135
|
+
},
|
|
136
|
+
[queueSave],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
value,
|
|
141
|
+
path,
|
|
142
|
+
loading,
|
|
143
|
+
saving,
|
|
144
|
+
error,
|
|
145
|
+
saveValue,
|
|
146
|
+
updateValue,
|
|
147
|
+
reload,
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/bridge/storage.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export type PlatformStorageJsonFileName = `${string}.json`
|
|
2
|
-
|
|
3
|
-
export interface PlatformStorageJsonRequest {
|
|
4
|
-
appSlug: string
|
|
5
|
-
fileName: PlatformStorageJsonFileName
|
|
6
|
-
}
|
|
1
|
+
export type PlatformStorageJsonFileName = `${string}.json`
|
|
2
|
+
|
|
3
|
+
export interface PlatformStorageJsonRequest {
|
|
4
|
+
appSlug: string
|
|
5
|
+
fileName: PlatformStorageJsonFileName
|
|
6
|
+
}
|
|
7
7
|
|
|
8
8
|
export interface PlatformStorageJsonReadResult {
|
|
9
9
|
path: string
|
|
@@ -20,7 +20,7 @@ beforeEach(() => {
|
|
|
20
20
|
|
|
21
21
|
describe('storage bridge helpers', () => {
|
|
22
22
|
it('reads JSON storage using the shell request object shape', async () => {
|
|
23
|
-
const request = { appSlug: 'puretasks', fileName: 'tasks.json' } as const
|
|
23
|
+
const request = { appSlug: 'puretasks', fileName: 'tasks.json' } as const
|
|
24
24
|
bridgeCall.mockResolvedValue({ path: '/tmp/tasks.json', value: null })
|
|
25
25
|
|
|
26
26
|
await readPlatformStorageJson(request)
|
|
@@ -32,11 +32,11 @@ describe('storage bridge helpers', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
it('writes JSON storage using the shell request object shape', async () => {
|
|
35
|
-
const request = {
|
|
36
|
-
appSlug: 'puretasks',
|
|
37
|
-
fileName: 'tasks.json',
|
|
38
|
-
value: { tasks: [] },
|
|
39
|
-
} as const
|
|
35
|
+
const request = {
|
|
36
|
+
appSlug: 'puretasks',
|
|
37
|
+
fileName: 'tasks.json',
|
|
38
|
+
value: { tasks: [] },
|
|
39
|
+
} as const
|
|
40
40
|
bridgeCall.mockResolvedValue({ path: '/tmp/tasks.json', ok: true })
|
|
41
41
|
|
|
42
42
|
await writePlatformStorageJson(request)
|
package/src/bridge/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type PlatformBridgeEventName =
|
|
|
2
2
|
import('./events.mjs').PlatformBridgeEventName
|
|
3
3
|
|
|
4
4
|
import type { PlatformThemeMode } from '../theme/tokens.js'
|
|
5
|
+
import type { PlatformAppContextManifest } from '../context/contextConfig.js'
|
|
5
6
|
|
|
6
7
|
/** Shell prefs exposed via `settings.prefs.get` / `settings.prefs.set`. */
|
|
7
8
|
export type PlatformThemePreference = PlatformThemeMode
|
|
@@ -80,6 +81,8 @@ export interface PlatformAppManifest {
|
|
|
80
81
|
productName: string
|
|
81
82
|
kind: string
|
|
82
83
|
description: string
|
|
84
|
+
/** Declarative context-file config for this app's package objects. */
|
|
85
|
+
context?: PlatformAppContextManifest
|
|
83
86
|
peopleProviders?: Array<{
|
|
84
87
|
id: string
|
|
85
88
|
label: string
|
|
@@ -205,6 +208,10 @@ export interface RenderPagedSnapshotMetrics {
|
|
|
205
208
|
captionVisible?: boolean
|
|
206
209
|
captionOrphaned?: boolean
|
|
207
210
|
strandedHeading?: boolean | null
|
|
211
|
+
/** Blocks on the target page that collide with each other. */
|
|
212
|
+
overlappingBlocks?: number
|
|
213
|
+
/** Blocks escaping the page content box. */
|
|
214
|
+
overflowingBlocks?: number
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
export interface RenderPagedPreviewResult {
|
|
@@ -45,6 +45,7 @@ export function AgentDrawerPanel({
|
|
|
45
45
|
drawerOpen = true,
|
|
46
46
|
composerPortaled = false,
|
|
47
47
|
threadHeader = null,
|
|
48
|
+
threadHeaderHeight = 34,
|
|
48
49
|
}: AgentDrawerPanelProps): React.ReactElement {
|
|
49
50
|
const awaitingApproval = pendingToolCalls.length > 0
|
|
50
51
|
const composerBlocked = composerDisabled
|
|
@@ -85,6 +86,7 @@ export function AgentDrawerPanel({
|
|
|
85
86
|
messages={messages}
|
|
86
87
|
assistantLabel={assistantLabel}
|
|
87
88
|
threadHeader={threadHeader}
|
|
89
|
+
threadHeaderHeight={threadHeaderHeight}
|
|
88
90
|
/>
|
|
89
91
|
</StyledAgentThreadHost>
|
|
90
92
|
</StyledAgentBody>
|
|
@@ -15,6 +15,7 @@ export interface AgentMessageListProps {
|
|
|
15
15
|
assistantLabel?: string
|
|
16
16
|
emptyMessage?: string
|
|
17
17
|
threadHeader?: React.ReactNode
|
|
18
|
+
threadHeaderHeight?: number
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const AgentMessageList = memo(function AgentMessageList({
|
|
@@ -22,11 +23,15 @@ export const AgentMessageList = memo(function AgentMessageList({
|
|
|
22
23
|
assistantLabel = 'assistant',
|
|
23
24
|
emptyMessage = 'Ask the agent about your workspace. Tool calls wait for your approval.',
|
|
24
25
|
threadHeader = null,
|
|
26
|
+
threadHeaderHeight = 34,
|
|
25
27
|
}: AgentMessageListProps): React.ReactElement {
|
|
26
28
|
const scrollKey = messages.map(message => message.id).join('|')
|
|
27
29
|
|
|
28
30
|
return (
|
|
29
|
-
<StyledAgentScroll
|
|
31
|
+
<StyledAgentScroll
|
|
32
|
+
data-thread-header={threadHeader ? '' : undefined}
|
|
33
|
+
$threadHeaderHeight={threadHeaderHeight}
|
|
34
|
+
>
|
|
30
35
|
{threadHeader}
|
|
31
36
|
<ChatThread
|
|
32
37
|
scrollKey={scrollKey}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { Text } from '../common/typography/Text.js'
|
|
2
|
-
import type { AgentUiToolCall } from './agentTypes.js'
|
|
1
|
+
import { Text } from '../common/typography/Text.js'
|
|
2
|
+
import type { AgentUiToolCall } from './agentTypes.js'
|
|
3
3
|
import {
|
|
4
4
|
formatAgentToolLabel,
|
|
5
5
|
formatAgentToolSummary,
|
|
6
6
|
} from './agentToolDisplay.js'
|
|
7
|
-
import { formatToolCallArguments } from './formatToolCallArguments.js'
|
|
8
|
-
import { StyledToolArgs, StyledToolCard } from './agentPanelStyles.js'
|
|
7
|
+
import { formatToolCallArguments } from './formatToolCallArguments.js'
|
|
8
|
+
import { StyledToolArgs, StyledToolCard } from './agentPanelStyles.js'
|
|
9
9
|
|
|
10
|
-
export interface AgentToolCallCardProps {
|
|
11
|
-
toolCall: AgentUiToolCall
|
|
12
|
-
footer?: React.ReactNode
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function AgentToolCallCard({
|
|
16
|
-
toolCall,
|
|
17
|
-
footer,
|
|
10
|
+
export interface AgentToolCallCardProps {
|
|
11
|
+
toolCall: AgentUiToolCall
|
|
12
|
+
footer?: React.ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AgentToolCallCard({
|
|
16
|
+
toolCall,
|
|
17
|
+
footer,
|
|
18
18
|
}: AgentToolCallCardProps): React.ReactElement {
|
|
19
19
|
return (
|
|
20
20
|
<StyledToolCard>
|
|
21
21
|
<Text weight="medium" size="sm">
|
|
22
22
|
{formatAgentToolLabel(toolCall.name)}
|
|
23
23
|
</Text>
|
|
24
|
-
<StyledToolArgs>
|
|
25
|
-
{formatAgentToolSummary(toolCall.name, toolCall.arguments) ||
|
|
26
|
-
formatToolCallArguments(toolCall.arguments)}
|
|
27
|
-
</StyledToolArgs>
|
|
28
|
-
{footer}
|
|
29
|
-
</StyledToolCard>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
24
|
+
<StyledToolArgs>
|
|
25
|
+
{formatAgentToolSummary(toolCall.name, toolCall.arguments) ||
|
|
26
|
+
formatToolCallArguments(toolCall.arguments)}
|
|
27
|
+
</StyledToolArgs>
|
|
28
|
+
{footer}
|
|
29
|
+
</StyledToolCard>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextPicker — Cmd-K-style overlay for choosing an object whose markdown
|
|
3
|
+
* context document should be attached as agent context.
|
|
4
|
+
*
|
|
5
|
+
* Generic on purpose: `search` and `onPick` are injected, so the shell
|
|
6
|
+
* drawer (IPC-backed) and plugin apps (bridge-backed `context.search`) use
|
|
7
|
+
* the same component — and agents use the same backing functions via the
|
|
8
|
+
* built-in `harness.context_search` / `harness.context_read` tools.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
12
|
+
import { styled } from 'styled-components'
|
|
13
|
+
|
|
14
|
+
import type { PlatformContextSearchHit } from '../../context/contextAccess.js'
|
|
15
|
+
import { SearchField } from '../common/inputs/SearchField.js'
|
|
16
|
+
|
|
17
|
+
export interface ContextPickerProps {
|
|
18
|
+
open: boolean
|
|
19
|
+
onClose: () => void
|
|
20
|
+
/** Search context-enabled objects — empty query lists everything. */
|
|
21
|
+
search: (query: string) => Promise<PlatformContextSearchHit[]>
|
|
22
|
+
/** Called with the chosen object; owner fetches markdown via context.read. */
|
|
23
|
+
onPick: (hit: PlatformContextSearchHit) => void | Promise<void>
|
|
24
|
+
title?: string
|
|
25
|
+
placeholder?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Backdrop = styled.div`
|
|
29
|
+
position: fixed;
|
|
30
|
+
inset: 0;
|
|
31
|
+
z-index: 1000;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: flex-start;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
padding-top: 12vh;
|
|
36
|
+
background: color-mix(
|
|
37
|
+
in srgb,
|
|
38
|
+
var(--platform-color-background, #101014) 45%,
|
|
39
|
+
transparent
|
|
40
|
+
);
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const Panel = styled.div`
|
|
44
|
+
width: min(560px, calc(100vw - 48px));
|
|
45
|
+
max-height: 60vh;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
padding: 12px;
|
|
50
|
+
border-radius: 12px;
|
|
51
|
+
border: 1px solid var(--platform-color-border, #2a2a32);
|
|
52
|
+
background: var(--platform-color-surface, #17171c);
|
|
53
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35);
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
const PanelTitle = styled.div`
|
|
57
|
+
font-size: 11px;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
text-transform: uppercase;
|
|
60
|
+
letter-spacing: 0.06em;
|
|
61
|
+
color: var(--platform-color-text-muted, #8b8b95);
|
|
62
|
+
padding: 2px 4px 0;
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
const Results = styled.ul`
|
|
66
|
+
list-style: none;
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: 0;
|
|
69
|
+
overflow-y: auto;
|
|
70
|
+
min-height: 0;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: 2px;
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
const ResultRow = styled.li<{ $active: boolean }>`
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 8px;
|
|
80
|
+
padding: 8px 10px;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
background: ${({ $active }) =>
|
|
84
|
+
$active
|
|
85
|
+
? 'var(--platform-color-surface-raised, #22222a)'
|
|
86
|
+
: 'transparent'};
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
const ResultTitle = styled.span`
|
|
90
|
+
flex: 1;
|
|
91
|
+
min-width: 0;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-overflow: ellipsis;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
font-weight: 500;
|
|
97
|
+
color: var(--platform-color-text, #e8e8ee);
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
const KindBadge = styled.span`
|
|
101
|
+
flex: none;
|
|
102
|
+
font-size: 11px;
|
|
103
|
+
font-weight: 400;
|
|
104
|
+
padding: 1px 8px;
|
|
105
|
+
border-radius: 999px;
|
|
106
|
+
border: 1px solid var(--platform-color-border, #2a2a32);
|
|
107
|
+
color: var(--platform-color-text-muted, #8b8b95);
|
|
108
|
+
`
|
|
109
|
+
|
|
110
|
+
const EmptyNote = styled.div`
|
|
111
|
+
padding: 14px 10px;
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
color: var(--platform-color-text-muted, #8b8b95);
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
export function ContextPicker({
|
|
117
|
+
open,
|
|
118
|
+
onClose,
|
|
119
|
+
search,
|
|
120
|
+
onPick,
|
|
121
|
+
title = 'Add context',
|
|
122
|
+
placeholder = 'Search manuscripts, books, documents…',
|
|
123
|
+
}: ContextPickerProps): React.ReactElement | null {
|
|
124
|
+
const [query, setQuery] = useState('')
|
|
125
|
+
const [hits, setHits] = useState<PlatformContextSearchHit[]>([])
|
|
126
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
127
|
+
const [searching, setSearching] = useState(false)
|
|
128
|
+
const requestSeq = useRef(0)
|
|
129
|
+
|
|
130
|
+
const runSearch = useCallback(
|
|
131
|
+
(value: string) => {
|
|
132
|
+
const seq = ++requestSeq.current
|
|
133
|
+
setSearching(true)
|
|
134
|
+
void search(value)
|
|
135
|
+
.then(results => {
|
|
136
|
+
if (requestSeq.current !== seq) return
|
|
137
|
+
setHits(results)
|
|
138
|
+
setActiveIndex(0)
|
|
139
|
+
})
|
|
140
|
+
.catch(() => {
|
|
141
|
+
if (requestSeq.current !== seq) return
|
|
142
|
+
setHits([])
|
|
143
|
+
})
|
|
144
|
+
.finally(() => {
|
|
145
|
+
if (requestSeq.current === seq) setSearching(false)
|
|
146
|
+
})
|
|
147
|
+
},
|
|
148
|
+
[search],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!open) return
|
|
153
|
+
setQuery('')
|
|
154
|
+
runSearch('')
|
|
155
|
+
}, [open, runSearch])
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!open) return
|
|
159
|
+
const timer = window.setTimeout(() => runSearch(query), 140)
|
|
160
|
+
return () => window.clearTimeout(timer)
|
|
161
|
+
}, [open, query, runSearch])
|
|
162
|
+
|
|
163
|
+
const pick = useCallback(
|
|
164
|
+
(hit: PlatformContextSearchHit | undefined) => {
|
|
165
|
+
if (!hit) return
|
|
166
|
+
void onPick(hit)
|
|
167
|
+
onClose()
|
|
168
|
+
},
|
|
169
|
+
[onClose, onPick],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const onKeyDown = useCallback(
|
|
173
|
+
(event: React.KeyboardEvent) => {
|
|
174
|
+
if (event.key === 'Escape') {
|
|
175
|
+
event.preventDefault()
|
|
176
|
+
onClose()
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
if (event.key === 'ArrowDown') {
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
setActiveIndex(index => Math.min(index + 1, hits.length - 1))
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (event.key === 'ArrowUp') {
|
|
185
|
+
event.preventDefault()
|
|
186
|
+
setActiveIndex(index => Math.max(index - 1, 0))
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (event.key === 'Enter') {
|
|
190
|
+
event.preventDefault()
|
|
191
|
+
pick(hits[activeIndex])
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[activeIndex, hits, onClose, pick],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if (!open) return null
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<Backdrop
|
|
201
|
+
onMouseDown={event => {
|
|
202
|
+
if (event.target === event.currentTarget) onClose()
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<Panel role="dialog" aria-label={title} onKeyDown={onKeyDown}>
|
|
206
|
+
<PanelTitle>{title}</PanelTitle>
|
|
207
|
+
<SearchField
|
|
208
|
+
value={query}
|
|
209
|
+
onValueChange={setQuery}
|
|
210
|
+
placeholder={placeholder}
|
|
211
|
+
autoFocus
|
|
212
|
+
/>
|
|
213
|
+
<Results role="listbox">
|
|
214
|
+
{hits.map((hit, index) => (
|
|
215
|
+
<ResultRow
|
|
216
|
+
key={hit.packagePath}
|
|
217
|
+
role="option"
|
|
218
|
+
aria-selected={index === activeIndex}
|
|
219
|
+
$active={index === activeIndex}
|
|
220
|
+
onMouseEnter={() => setActiveIndex(index)}
|
|
221
|
+
onMouseDown={event => {
|
|
222
|
+
event.preventDefault()
|
|
223
|
+
pick(hit)
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<ResultTitle>{hit.title}</ResultTitle>
|
|
227
|
+
<KindBadge>{hit.kind}</KindBadge>
|
|
228
|
+
</ResultRow>
|
|
229
|
+
))}
|
|
230
|
+
{hits.length === 0 ? (
|
|
231
|
+
<EmptyNote>
|
|
232
|
+
{searching ? 'Searching…' : 'No matching objects.'}
|
|
233
|
+
</EmptyNote>
|
|
234
|
+
) : null}
|
|
235
|
+
</Results>
|
|
236
|
+
</Panel>
|
|
237
|
+
</Backdrop>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
@@ -23,7 +23,7 @@ export const StyledAgentThreadHost = styled.div`
|
|
|
23
23
|
width: 100%;
|
|
24
24
|
`
|
|
25
25
|
|
|
26
|
-
export const StyledAgentScroll = styled.div
|
|
26
|
+
export const StyledAgentScroll = styled.div<{ $threadHeaderHeight: number }>`
|
|
27
27
|
position: relative;
|
|
28
28
|
min-height: 0;
|
|
29
29
|
display: flex;
|
|
@@ -31,7 +31,8 @@ export const StyledAgentScroll = styled.div`
|
|
|
31
31
|
--platform-agent-thread-header-height: 0px;
|
|
32
32
|
|
|
33
33
|
&[data-thread-header] {
|
|
34
|
-
--platform-agent-thread-header-height:
|
|
34
|
+
--platform-agent-thread-header-height: ${({ $threadHeaderHeight }) =>
|
|
35
|
+
$threadHeaderHeight}px;
|
|
35
36
|
}
|
|
36
37
|
`
|
|
37
38
|
|
|
@@ -136,4 +136,6 @@ export interface AgentDrawerPanelProps {
|
|
|
136
136
|
composerPortaled?: boolean
|
|
137
137
|
/** Shell thread chrome (scope, session actions) — anchored to the message column. */
|
|
138
138
|
threadHeader?: React.ReactNode
|
|
139
|
+
/** Reserved top space for an absolute thread header. */
|
|
140
|
+
threadHeaderHeight?: number
|
|
139
141
|
}
|
|
@@ -169,6 +169,20 @@ const StyledDocName = styled.span`
|
|
|
169
169
|
color: var(--pure-chrome-tertiary, #8a857c);
|
|
170
170
|
`
|
|
171
171
|
|
|
172
|
+
const StyledDevBadge = styled.span`
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
justify-content: center;
|
|
176
|
+
flex-shrink: 0;
|
|
177
|
+
font-size: 9px;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
line-height: 1;
|
|
180
|
+
letter-spacing: 0.04em;
|
|
181
|
+
text-transform: uppercase;
|
|
182
|
+
color: var(--pure-chrome-tertiary, #8a857c);
|
|
183
|
+
opacity: 0.72;
|
|
184
|
+
`
|
|
185
|
+
|
|
172
186
|
const StyledClose = styled(X)`
|
|
173
187
|
cursor: pointer;
|
|
174
188
|
opacity: 0.7;
|
|
@@ -226,7 +240,7 @@ export function WorkspaceTabStrip({
|
|
|
226
240
|
<StyledScroll role="tablist" aria-label={ariaLabel}>
|
|
227
241
|
{tabs.map((tab, index) => {
|
|
228
242
|
const active = tab.id === activeTabId
|
|
229
|
-
const closable =
|
|
243
|
+
const closable = onClose
|
|
230
244
|
return (
|
|
231
245
|
<StyledTab
|
|
232
246
|
key={tab.id}
|
|
@@ -306,6 +320,9 @@ export function WorkspaceTabStrip({
|
|
|
306
320
|
tab.label
|
|
307
321
|
)}
|
|
308
322
|
</StyledLabel>
|
|
323
|
+
{tab.underDevelopment ? (
|
|
324
|
+
<StyledDevBadge title="Under development">dev</StyledDevBadge>
|
|
325
|
+
) : null}
|
|
309
326
|
{closable ? (
|
|
310
327
|
<StyledClose
|
|
311
328
|
type="button"
|
|
@@ -62,10 +62,10 @@ describe('AppFrame', () => {
|
|
|
62
62
|
|
|
63
63
|
expect(document.getElementById(MENU_PORTAL_ID)).not.toBeNull()
|
|
64
64
|
const baseStyles = document.getElementById(PLATFORM_APP_FRAME_BASE_STYLE_ID)
|
|
65
|
-
expect(baseStyles).not.toBeNull()
|
|
66
|
-
expect(baseStyles?.textContent).toContain('box-sizing: border-box')
|
|
67
|
-
expect(baseStyles?.textContent).toContain('scrollbar-color')
|
|
68
|
-
expect(document.getElementById(PLATFORM_FONT_STYLESHEET_ID)).not.toBeNull()
|
|
65
|
+
expect(baseStyles).not.toBeNull()
|
|
66
|
+
expect(baseStyles?.textContent).toContain('box-sizing: border-box')
|
|
67
|
+
expect(baseStyles?.textContent).toContain('scrollbar-color')
|
|
68
|
+
expect(document.getElementById(PLATFORM_FONT_STYLESHEET_ID)).not.toBeNull()
|
|
69
69
|
expect(container.querySelector('[data-app="tasks"]')).not.toBeNull()
|
|
70
70
|
expect(document.documentElement.dataset.app).toBe('tasks')
|
|
71
71
|
expect(container.textContent).toContain('App content')
|
|
@@ -15,6 +15,8 @@ export interface AppFrameProps
|
|
|
15
15
|
themeFromBridge?: boolean
|
|
16
16
|
/** Optional actions rendered in the app header's action slot. */
|
|
17
17
|
headerActions?: ReactNode
|
|
18
|
+
/** Open document's file name, shown beside the wordmark. */
|
|
19
|
+
headerDocumentName?: ReactNode
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const StyledRoot = styled.div`
|
|
@@ -37,6 +39,7 @@ export function AppFrame({
|
|
|
37
39
|
className,
|
|
38
40
|
themeFromBridge = true,
|
|
39
41
|
headerActions,
|
|
42
|
+
headerDocumentName,
|
|
40
43
|
...rest
|
|
41
44
|
}: AppFrameProps): React.ReactElement {
|
|
42
45
|
const { meta, ready } = usePlatformBridge()
|
|
@@ -73,7 +76,13 @@ export function AppFrame({
|
|
|
73
76
|
data-platform-surface-palette={themeAttrs.surfacePalette || undefined}
|
|
74
77
|
{...rest}
|
|
75
78
|
>
|
|
76
|
-
{manifest &&
|
|
79
|
+
{manifest && (
|
|
80
|
+
<AppHeader
|
|
81
|
+
manifest={manifest}
|
|
82
|
+
actions={headerActions}
|
|
83
|
+
documentName={headerDocumentName}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
77
86
|
{children}
|
|
78
87
|
</StyledRoot>
|
|
79
88
|
</>
|