Skip to content

Commit abf7f97

Browse files
authored
feat(core): add app context sources (#246)
1 parent 9958069 commit abf7f97

64 files changed

Lines changed: 4669 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,13 @@ askable-ui is the context layer. It doesn't replace your LLM SDK — it gives it
158158
| Package | Version | Use when |
159159
|---|---|---|
160160
| [`@askable-ui/core`](https://www.npmjs.com/package/@askable-ui/core) | [![npm](https://img.shields.io/npm/v/@askable-ui/core?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/core) | Vanilla JS, custom framework, or as a peer dep |
161-
| [`@askable-ui/context`](https://www.npmjs.com/package/@askable-ui/context) | npm package | Shared Context packet types, schema, and validators |
161+
| [`@askable-ui/context`](https://www.npmjs.com/package/@askable-ui/context) | [![npm](https://img.shields.io/npm/v/@askable-ui/context?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/context) | Shared Context packet types, schema, and validators |
162162
| [`@askable-ui/react`](https://www.npmjs.com/package/@askable-ui/react) | [![npm](https://img.shields.io/npm/v/@askable-ui/react?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/react) | React 18+ |
163163
| [`@askable-ui/react-native`](https://www.npmjs.com/package/@askable-ui/react-native) | [![npm](https://img.shields.io/npm/v/@askable-ui/react-native?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/react-native) | React Native (initial press-driven adapter) |
164164
| [`@askable-ui/vue`](https://www.npmjs.com/package/@askable-ui/vue) | [![npm](https://img.shields.io/npm/v/@askable-ui/vue?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/vue) | Vue 3 |
165165
| [`@askable-ui/svelte`](https://www.npmjs.com/package/@askable-ui/svelte) | [![npm](https://img.shields.io/npm/v/@askable-ui/svelte?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/svelte) | Svelte 4 & 5 |
166-
| [`@askable-ui/mcp`](https://www.npmjs.com/package/@askable-ui/mcp) | npm package | MCP bridge for exposing Context packets to agents |
167-
| [`@askable-ui/create-app`](https://www.npmjs.com/package/@askable-ui/create-app) | npm package | React + Vite + CopilotKit starter scaffold |
166+
| [`@askable-ui/mcp`](https://www.npmjs.com/package/@askable-ui/mcp) | [![npm](https://img.shields.io/npm/v/@askable-ui/mcp?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/mcp) | MCP bridge for exposing Context packets to agents |
167+
| [`@askable-ui/create-app`](https://www.npmjs.com/package/@askable-ui/create-app) | [![npm](https://img.shields.io/npm/v/@askable-ui/create-app?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/create-app) | React + Vite + CopilotKit starter scaffold |
168168

169169
<details>
170170
<summary><strong>Framework quick starts</strong></summary>

examples/analytics-dashboard-react/components/askable-interaction-toolbar.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,14 @@ export function AskableInteractionToolbar() {
6161
ctx,
6262
source: { app: "analytics-dashboard-demo" },
6363
onCapture(packet, selection) {
64-
const label = `Highlighted text: ${selection.text}`
6564
ctx.push(
6665
{
6766
capture: packet.capture.mode,
6867
gesture: packet.capture.gesture ?? "programmatic",
6968
length: selection.text.length,
7069
...(selection.bounds ? { bounds: selection.bounds } : {}),
7170
},
72-
label,
71+
selection.text,
7372
)
7473
setStatus(`Sent ${selection.text.length} selected characters to chat context.`)
7574
resumeImplicitFocus()

examples/analytics-dashboard-react/components/dashboard/chat-sidebar.tsx

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client"
22

33
import { useState, useEffect } from "react"
4-
import { Send, Sparkles, X, Maximize2, Minimize2, Eye } from "lucide-react"
4+
import { Send, Sparkles, X, Maximize2, Minimize2, Eye, Quote } from "lucide-react"
55
import { useAskable } from "@askable-ui/react"
66
import { Button } from "@/components/ui/button"
77
import { Input } from "@/components/ui/input"
@@ -15,6 +15,8 @@ interface Message {
1515
timestamp: Date
1616
context?: string
1717
isContext?: boolean
18+
contextKind?: "text" | "ui"
19+
selectedText?: string
1820
}
1921

2022
const initialMessages: Message[] = [
@@ -33,7 +35,9 @@ export function ChatSidebar() {
3335
const [isMinimized, setIsMinimized] = useState(false)
3436
const [showContext, setShowContext] = useState(true)
3537

36-
const { promptContext } = useAskable({ inspector: true });
38+
const { promptContext, focus } = useAskable({ inspector: true })
39+
const hasSelectedText = isTextSelectionMeta(focus?.meta)
40+
const selectedText = hasSelectedText ? focus?.text ?? "" : ""
3741

3842
const handleSend = () => {
3943
if (!input.trim()) return
@@ -44,6 +48,8 @@ export function ChatSidebar() {
4448
content: input,
4549
timestamp: new Date(),
4650
context: promptContext || undefined,
51+
contextKind: hasSelectedText ? "text" : "ui",
52+
selectedText,
4753
}
4854

4955
setMessages((prev) => [...prev, userMessage])
@@ -56,6 +62,8 @@ export function ChatSidebar() {
5662
content: promptContext,
5763
timestamp: new Date(),
5864
isContext: true,
65+
contextKind: hasSelectedText ? "text" : "ui",
66+
selectedText,
5967
}
6068
setMessages((prev) => [...prev, assistantMessage])
6169
}
@@ -119,26 +127,43 @@ export function ChatSidebar() {
119127
{/* Context Panel - Main Feature Showcase */}
120128
<div className="border-b border-border">
121129
{promptContext ? (
122-
<div className="bg-gradient-to-b from-emerald-500/10 to-emerald-500/5 p-4">
130+
<div className={cn(
131+
"p-4",
132+
hasSelectedText
133+
? "bg-gradient-to-b from-violet-500/10 to-violet-500/5"
134+
: "bg-gradient-to-b from-emerald-500/10 to-emerald-500/5"
135+
)}>
123136
<div className="mb-3 flex items-center justify-between">
124137
<div className="flex items-center gap-2">
125-
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500">
126-
<Eye className="h-4 w-4 text-white" />
138+
<div className={cn(
139+
"flex h-7 w-7 items-center justify-center rounded-full",
140+
hasSelectedText ? "bg-violet-500" : "bg-emerald-500"
141+
)}>
142+
{hasSelectedText ? <Quote className="h-4 w-4 text-white" /> : <Eye className="h-4 w-4 text-white" />}
127143
</div>
128144
<div>
129-
<span className="text-sm font-semibold text-emerald-400">Context Captured</span>
130-
<p className="text-xs text-emerald-400/70">AI can see this element</p>
145+
<span className={cn("text-sm font-semibold", hasSelectedText ? "text-violet-400" : "text-emerald-400")}>
146+
{hasSelectedText ? "Selected Text Captured" : "Context Captured"}
147+
</span>
148+
<p className={cn("text-xs", hasSelectedText ? "text-violet-400/70" : "text-emerald-400/70")}>
149+
{hasSelectedText ? "This exact highlight will be sent to chat" : "AI can see this element"}
150+
</p>
131151
</div>
132152
</div>
133153
<button
134154
onClick={() => setShowContext(!showContext)}
135-
className="rounded-md border border-emerald-500/30 bg-emerald-500/10 px-2 py-1 text-xs font-medium text-emerald-400 transition-colors hover:bg-emerald-500/20"
155+
className={cn(
156+
"rounded-md border px-2 py-1 text-xs font-medium transition-colors",
157+
hasSelectedText
158+
? "border-violet-500/30 bg-violet-500/10 text-violet-400 hover:bg-violet-500/20"
159+
: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20"
160+
)}
136161
>
137162
{showContext ? "Collapse" : "Expand"}
138163
</button>
139164
</div>
140165
{showContext && (
141-
<ContextPanel content={promptContext} />
166+
<ContextPanel content={promptContext} selectedText={selectedText} />
142167
)}
143168
</div>
144169
) : (
@@ -176,13 +201,20 @@ export function ChatSidebar() {
176201
)}
177202
<div className="max-w-[85%]">
178203
{message.context && message.role === "user" && (
179-
<div className="mb-1 flex items-center gap-1 text-xs text-chart-1">
180-
<Eye className="h-3 w-3" />
181-
<span>with context</span>
204+
<div className={cn(
205+
"mb-1 flex items-center gap-1 text-xs",
206+
message.contextKind === "text" ? "text-violet-400" : "text-chart-1"
207+
)}>
208+
{message.contextKind === "text" ? <Quote className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
209+
<span>{message.contextKind === "text" ? "with selected text" : "with context"}</span>
182210
</div>
183211
)}
184212
{message.isContext ? (
185-
<ContextCard content={message.content} />
213+
<ContextCard
214+
content={message.content}
215+
kind={message.contextKind ?? "ui"}
216+
selectedText={message.selectedText}
217+
/>
186218
) : (
187219
<div
188220
className={cn(
@@ -247,6 +279,14 @@ function parseContext(content: string): Record<string, unknown> | string[] | nul
247279
return lines.length > 1 ? lines : null
248280
}
249281

282+
function isTextSelectionMeta(meta: unknown) {
283+
return Boolean(
284+
meta &&
285+
typeof meta === "object" &&
286+
(meta as Record<string, unknown>).capture === "text-selection"
287+
)
288+
}
289+
250290
function renderContextValue(value: unknown, tone: "panel" | "card" = "panel") {
251291
if (value !== null && typeof value === "object") {
252292
return (
@@ -266,7 +306,28 @@ function renderContextValue(value: unknown, tone: "panel" | "card" = "panel") {
266306
return <span className="text-xs font-medium text-foreground break-words whitespace-pre-wrap">{String(value)}</span>
267307
}
268308

269-
function ContextPanel({ content }: { content: string }) {
309+
function SelectedTextPreview({ text, tone = "panel" }: { text: string; tone?: "panel" | "card" }) {
310+
return (
311+
<div className={cn(
312+
"rounded-lg border p-3",
313+
tone === "panel"
314+
? "border-violet-500/20 bg-background/80"
315+
: "border-violet-500/30 bg-violet-500/10"
316+
)}>
317+
<div className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-violet-400">
318+
<Quote className="h-3.5 w-3.5" />
319+
<span>Selected text passed to chat</span>
320+
</div>
321+
<blockquote className="text-xs leading-relaxed text-foreground break-words whitespace-pre-wrap">
322+
{text}
323+
</blockquote>
324+
</div>
325+
)
326+
}
327+
328+
function ContextPanel({ content, selectedText }: { content: string; selectedText?: string }) {
329+
if (selectedText) return <SelectedTextPreview text={selectedText} />
330+
270331
const parsed = parseContext(content)
271332

272333
if (parsed && !Array.isArray(parsed)) {
@@ -299,7 +360,17 @@ function ContextPanel({ content }: { content: string }) {
299360
)
300361
}
301362

302-
function ContextCard({ content }: { content: string }) {
363+
function ContextCard({
364+
content,
365+
kind = "ui",
366+
selectedText,
367+
}: {
368+
content: string
369+
kind?: "text" | "ui"
370+
selectedText?: string
371+
}) {
372+
if (kind === "text" && selectedText) return <SelectedTextPreview text={selectedText} tone="card" />
373+
303374
const parsed = parseContext(content)
304375
const entries = parsed && !Array.isArray(parsed) ? Object.entries(parsed) : null
305376
const lines = Array.isArray(parsed) ? parsed : content.split(/\s*\s*/).filter(Boolean)

examples/analytics-dashboard-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"start": "next start"
1010
},
1111
"dependencies": {
12-
"@askable-ui/react": "^0.11.1",
12+
"@askable-ui/react": "^0.12.0",
1313
"@hookform/resolvers": "^3.9.1",
1414
"@radix-ui/react-accordion": "1.2.12",
1515
"@radix-ui/react-alert-dialog": "1.1.15",

examples/react-native-expo/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@askable-ui/example-react-native-expo",
33
"private": true,
4-
"version": "0.0.0",
4+
"version": "0.12.0",
55
"main": "expo/AppEntry",
66
"scripts": {
77
"start": "expo start",
@@ -11,8 +11,8 @@
1111
"typecheck": "tsc --noEmit"
1212
},
1313
"dependencies": {
14-
"@askable-ui/core": "^0.11.1",
15-
"@askable-ui/react-native": "^0.11.1",
14+
"@askable-ui/core": "^0.12.0",
15+
"@askable-ui/react-native": "^0.12.0",
1616
"@react-navigation/native": "^7.2.5",
1717
"@react-navigation/native-stack": "^7.16.0",
1818
"expo": "^55.0.26",

package-lock.json

Lines changed: 17 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "askable",
3-
"version": "0.11.1",
3+
"version": "0.12.0",
44
"private": true,
55
"workspaces": [
66
"packages/*"

packages/context/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@askable-ui/context",
3-
"version": "0.11.1",
3+
"version": "0.12.0",
44
"description": "Open Context packet types and schema for AI-native interfaces",
55
"type": "module",
66
"main": "./dist/index.js",

packages/context/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface WebContextSurrounding {
8282
nearby?: WebContextTarget[];
8383
visible?: WebContextTarget[];
8484
history?: WebContextTarget[];
85+
sources?: WebContextTarget[];
8586
}
8687

8788
export interface WebContextPrivacy {
@@ -180,6 +181,7 @@ export const webContextPacketSchema = {
180181
nearby: { type: 'array', items: { $ref: '#/$defs/target' } },
181182
visible: { type: 'array', items: { $ref: '#/$defs/target' } },
182183
history: { type: 'array', items: { $ref: '#/$defs/target' } },
184+
sources: { type: 'array', items: { $ref: '#/$defs/target' } },
183185
},
184186
},
185187
privacy: {

0 commit comments

Comments
 (0)