Skip to content
Closed
5 changes: 4 additions & 1 deletion components.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}
28 changes: 26 additions & 2 deletions packages/agent-core/src/runtime-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
import type {
CellData,
ExecutionQueueData,
FileData,
RuntimeSessionData,
} from "@runtimed/schema";

Expand All @@ -46,7 +47,7 @@ export class RuntimeAgent {
activeExecutions = new Map<string, AbortController>();
cancellationHandlers: CancellationHandler[] = [];
renewalInterval?: ReturnType<typeof setInterval>;

fileUpload?: (id: string) => void;
artifactClient: IArtifactClient;

config: RuntimeConfig;
Expand All @@ -64,6 +65,10 @@ export class RuntimeAgent {
this.handlers = handlers;
}

onFileUpload(cb: (id: string) => void) {
this.fileUpload = cb;
}

/**
* Start the runtime agent - connects to LiveStore and begins processing
*/
Expand Down Expand Up @@ -295,6 +300,11 @@ export class RuntimeAgent {
}
);

// Watch for file uploads
const fileUploadedQuery$ = queryDb(tables.files.select(), {
label: "fileUploaded",
});

// Watch for cancelled executions
const cancelledWorkQuery$ = queryDb(
tables.executionQueue.select().where({ status: "cancelled" }),
Expand Down Expand Up @@ -434,13 +444,27 @@ export class RuntimeAgent {
},
});

const fileUploadedSub = this.store.subscribe(fileUploadedQuery$, {
onUpdate: (entries: readonly FileData[]) => {
if (this.isShuttingDown) return;
if (entries.length === 0) return;

console.log("uploaded files (from subscription)", entries);

for (const entry of entries) {
this.fileUpload?.(entry.id);
}
},
});

// Store subscriptions for cleanup
this.subscriptions.push(
assignedWorkSub,
pendingWorkSub,
cancelledWorkSub,
completedExecutionsSub,
failedExecutionsSub
failedExecutionsSub,
fileUploadedSub
);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/pyodide-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
KNOWN_MIME_TYPES,
type KnownMimeType,
maxAiIterations$,
tables,
} from "@runtimed/schema";

// Type guard for objects with string indexing
Expand Down Expand Up @@ -260,6 +261,26 @@ export class PyodideRuntimeAgent extends LocalRuntimeAgent {
// Initialize Pyodide worker
await this.initializePyodideWorker();

// Send uploaded files to worker
if (this.agent) {
const agent = this.agent;
const files = agent.store.query(tables.files.select());
if (files.length > 0) {
const filesWithUrls = files.map((file) => ({
...file,
url: agent.artifactClient.getArtifactUrl(file.id),
}));
this.sendWorkerMessage("files", { files: filesWithUrls });
this.agent.onFileUpload((id) => {
const files = agent.store.query(tables.files.select().where({ id }));
const file = files[0];
if (file) {
this.sendWorkerMessage("files", { files: [file] });
}
});
}
}

// Expose runtime agent globally for debugging
globalThis.__PYODIDE_RUNTIME_AGENT__ = this;
console.log(
Expand Down
17 changes: 17 additions & 0 deletions packages/pyodide-runtime/src/pyodide-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,23 @@ await run_registered_tool("${data.toolName}", kwargs_string)
break;
}

case "files": {
if (!pyodide) {
throw new Error("Pyodide not initialized");
}

for (const file of data.files) {
const response = await fetch(file.url);
const content = await response.text();
pyodide.FS.writeFile(`./${file.fileName}`, content);
}

console.log("worker wrote files", { data });

self.postMessage({ id, type: "response", data: { success: true } });
break;
}

case "debug": {
// Debug message handler - allows direct access to pyodide instance
try {
Expand Down
42 changes: 42 additions & 0 deletions packages/schema/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ export const events = {
}),
}),

fileUploaded: Events.synced({
name: "v1.FileUploaded",
schema: Schema.Struct({
artifactId: Schema.String,
mimeType: Schema.String,
fileName: Schema.String,
createdAt: Schema.Date,
createdBy: Schema.String,
}),
}),

fileDeleted: Events.synced({
name: "v1.FileDeleted",
schema: Schema.Struct({
id: Schema.String,
}),
}),

// Notebook events (single notebook per store)
/** @deprecated */
notebookInitialized: Events.synced({
Expand Down Expand Up @@ -672,6 +690,29 @@ export const materializers = State.SQLite.materializers(events, {
.onConflict("id", "replace"),
],

"v1.FileUploaded": ({
artifactId,
mimeType,
fileName,
createdBy,
createdAt,
}) => [
tables.files
.insert({
id: artifactId,
mimeType,
fileName,
createdBy,
createdAt,
})
// Don't overwrite existing ids
.onConflict("id", "ignore"),
// TODO: overwrite existing file names
// .onConflict(["notebookId", "fileName"], "replace"),
],

"v1.FileDeleted": ({ id }) => [tables.files.delete().where({ id })],

"v1.NotebookTitleChanged": ({ title }) =>
tables.notebookMetadata
.insert({
Expand Down Expand Up @@ -1298,6 +1339,7 @@ export type OutputData = typeof tables.outputs.Type;
export type RuntimeSessionData = typeof tables.runtimeSessions.Type;
export type ExecutionQueueData = typeof tables.executionQueue.Type;
export type UiStateData = typeof tables.uiState.Type;
export type FileData = typeof tables.files.Type;

// Type guards for MediaContainer
export function isInlineContainer<T>(
Expand Down
11 changes: 11 additions & 0 deletions packages/schema/src/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export const tables = {
},
}),

files: State.SQLite.table({
name: "files",
columns: {
id: State.SQLite.text({ primaryKey: true }),
mimeType: State.SQLite.text(),
fileName: State.SQLite.text(),
createdAt: State.SQLite.datetime({ nullable: true }),
createdBy: State.SQLite.text(),
},
}),

presence: State.SQLite.table({
name: "presence",
columns: {
Expand Down
141 changes: 141 additions & 0 deletions src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as React from "react"
import { ChevronRight, File, Folder } from "lucide-react"

import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarRail,
} from "@/components/ui/sidebar"

// This is sample data.
const data = {
changes: [
{
file: "README.md",
state: "M",
},
{
file: "api/hello/route.ts",
state: "U",
},
{
file: "app/layout.tsx",
state: "M",
},
],
tree: [
[
"app",
[
"api",
["hello", ["route.ts"]],
"page.tsx",
"layout.tsx",
["blog", ["page.tsx"]],
],
],
[
"components",
["ui", "button.tsx", "card.tsx"],
"header.tsx",
"footer.tsx",
],
["lib", ["util.ts"]],
["public", "favicon.ico", "vercel.svg"],
".eslintrc.json",
".gitignore",
"next.config.js",
"tailwind.config.js",
"package.json",
"README.md",
],
}

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Changes</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{data.changes.map((item, index) => (
<SidebarMenuItem key={index}>
<SidebarMenuButton>
<File />
{item.file}
</SidebarMenuButton>
<SidebarMenuBadge>{item.state}</SidebarMenuBadge>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Files</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{data.tree.map((item, index) => (
<Tree key={index} item={item} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
)
}

function Tree({ item }: { item: string | any[] }) {
const [name, ...items] = Array.isArray(item) ? item : [item]

if (!items.length) {
return (
<SidebarMenuButton
isActive={name === "button.tsx"}
className="data-[active=true]:bg-transparent"
>
<File />
{name}
</SidebarMenuButton>
)
}

return (
<SidebarMenuItem>
<Collapsible
className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90"
defaultOpen={name === "components" || name === "ui"}
>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<ChevronRight className="transition-transform" />
<Folder />
{name}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{items.map((subItem, index) => (
<Tree key={index} item={subItem} />
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
</SidebarMenuItem>
)
}
2 changes: 2 additions & 0 deletions src/components/notebook/EmptyStateCellAdder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SqlCellButton,
AiCellButton,
} from "./cell/CellTypeButtons";
import { CsvUploadButton } from "./cell/CsvUploadButton";

export const EmptyStateCellAdder: React.FC = () => {
const { addCell } = useAddCell();
Expand Down Expand Up @@ -44,6 +45,7 @@ export const EmptyStateCellAdder: React.FC = () => {
onClick={() => addCell(undefined, "ai")}
className="flex items-center gap-2"
/>
<CsvUploadButton size="lg" className="flex items-center gap-2" />
</div>
<div className="text-muted-foreground hidden text-xs sm:block">
💡 Real-time collaborative computing. Pick a cell type to start
Expand Down
2 changes: 2 additions & 0 deletions src/components/notebook/NotebookSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
RuntimePanel,
HelpPanel,
DebugPanel,
FilesPanel,
type SidebarSection,
type SidebarPanelProps,
} from "./sidebar-panels";
Expand All @@ -31,6 +32,7 @@ interface NotebookSidebarProps {

const PANEL_COMPONENTS: Record<SidebarSection, React.FC<SidebarPanelProps>> = {
metadata: MetadataPanel,
files: FilesPanel,
ai: AiPanel,
runtime: RuntimePanel,
debug: DebugPanel,
Expand Down
Loading
Loading