Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fb532ff
Try to add an app console and reproduce the error; Uncaught NotSuppor…
jlewi-openai Dec 21, 2025
e5a5bd5
Make custom element registration idempotent
jlewi-openai Dec 21, 2025
708d055
Guard console messaging when context is missing
jlewi-openai Dec 21, 2025
e20aa2f
Declare renderers as peer dependency for react-console
jlewi-openai Dec 21, 2025
144dfe7
Externalize runme libs and declare as peers
jlewi-openai Dec 21, 2025
79b7638
Tweak AppConsole welcome message
jlewi-openai Dec 21, 2025
4a54e27
Add stub prompt/echo behavior to AppConsole
jlewi-openai Dec 21, 2025
89380fe
Buffer AppConsole input and add welcome banner
jlewi-openai Dec 21, 2025
09643bb
Allow per-instance console context and wire RunmeConsole
jlewi-openai Dec 21, 2025
2a70530
Let each RunmeConsole use its own messaging context
jlewi-openai Dec 21, 2025
dc802e9
Add a commit message.
jlewi-openai Dec 21, 2025
cbe57d6
Remove defineCustomElement wrapper and restore decorators
jlewi-openai Dec 21, 2025
10be0d3
Remove unused getContext.
Dec 21, 2025
8fc23f5
Allow RunmeConsole to accept injected messaging context
Dec 21, 2025
b90a22f
Extract RunmeRenderContext and allow injected contexts
Dec 21, 2025
41dcc92
Start building a testApp.
Dec 21, 2025
7cdf28c
Revert RunmeRenderContext helper and inline context bridge
Dec 22, 2025
0de0fcd
Remove import of getContext.
Dec 22, 2025
7a1c3ab
Add testApp consoles using AppConsole stub
Dec 22, 2025
1730926
Expose Streams types and add StreamCreator hooks for testing
Dec 22, 2025
70890f7
Change definition of streams type to streamslike.
Dec 22, 2025
2896c59
Wire testApp consoles to FakeStreams via StreamCreator
Dec 22, 2025
bf7890c
Log which Streams factory RunmeConsole uses
Dec 22, 2025
7fa7d8b
Clean up testApp
Dec 22, 2025
e7a5be0
Add some debug logging; revert this later.
Dec 22, 2025
bbd816a
Export FakeStreams and use package fake in testApp
Dec 22, 2025
205b226
Add some more logging; testApp seems to be working but you need to re…
Dec 22, 2025
e87c99b
Add a README.
Dec 22, 2025
044f92f
Remove AppConsole from react-components.
Dec 22, 2025
fad5fc4
Fix the lock file.
Dec 22, 2025
1baf0b8
Changelog for jlewi dev release.
Dec 30, 2025
20ecf08
Revert most changes to the bare minimum to get this to work for myself.
Dec 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/react-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@runmedev/renderers": "workspace:*"
},
"peerDependencies": {
"@runmedev/renderers": "workspace:*",
"@bufbuild/protobuf": "^2.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand All @@ -41,4 +42,4 @@
"publishConfig": {
"access": "public"
}
}
}
13 changes: 12 additions & 1 deletion packages/react-console/src/components/Console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { useEffect, useMemo, useRef } from 'react'

import { Interceptor } from '@connectrpc/connect'
import '@runmedev/renderers'
import type { RunmeConsoleStream, ConsoleViewConfig } from '@runmedev/renderers'
import type {
RunmeConsoleStream,
ConsoleViewConfig,
StreamsProps,
StreamsLike,
} from '@runmedev/renderers'

interface ConsoleSettings {
rows?: number
Expand All @@ -29,6 +34,7 @@ export interface ConsoleProps {
content?: string
runner: ConsoleRunner
settings?: ConsoleSettings
streamCreator?: (props: StreamsProps) => StreamsLike
onStdout?: (data: Uint8Array) => void
onStderr?: (data: Uint8Array) => void
onExitCode?: (code: number) => void
Expand All @@ -45,6 +51,7 @@ function Console({
content,
runner,
settings: settingsProp = {},
streamCreator,
onStdout,
onStderr,
onExitCode,
Expand Down Expand Up @@ -88,6 +95,7 @@ function Console({
[cellID, fontFamily, fontSize, takeFocus, scrollToFit, rows, content]
)

console.log('Creating RunmeConsoleStream: rendering with cellID', cellID, 'runID', runID)
const webComponentStream: RunmeConsoleStream = useMemo(
() => ({
knownID: cellID,
Expand Down Expand Up @@ -170,6 +178,9 @@ function Console({
// Bypass attributes because serialization of funcs won't work
elem.interceptors = runner.interceptors
elem.commands = commands
if (streamCreator) {
elem.StreamCreator = streamCreator
}

el.appendChild(elem)
const terminalEnd = document.createElement('div')
Expand Down
5 changes: 5 additions & 0 deletions packages/renderers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
"types": "./dist/components.d.ts",
"import": "./dist/components.mjs",
"require": "./dist/components.cjs"
},
"./streams/fakeStreams": {
"types": "./dist/streams/fakeStreams.d.ts",
"import": "./dist/streams/fakeStreams.mjs",
"require": "./dist/streams/fakeStreams.cjs"
}
},
"files": [
Expand Down
34 changes: 29 additions & 5 deletions packages/renderers/src/components/console/runme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { type RendererContext } from 'vscode-notebook-renderer'
import { type VSCodeEvent } from 'vscode-notebook-renderer/events'

import { setContext } from '../../messaging'
import Streams from '../../streams'
import Streams, { type StreamsLike, type StreamsProps } from '../../streams'
import { ClientMessages } from '../../types'
import { ConsoleView, ConsoleViewConfig } from './view'

Expand All @@ -39,9 +39,10 @@ export class RunmeConsole extends LitElement {
protected consoleView?: ConsoleView

// Streams-specific state
#streams?: Streams
#streams?: StreamsLike
#streamsUnsubs: Array<() => void> = []
#winsize = { rows: 34, cols: 100, x: 0, y: 0 }
#contextBridge?: RendererContext<void>

// Properties delegated to ConsoleView
@property({ type: String })
Expand Down Expand Up @@ -81,9 +82,13 @@ export class RunmeConsole extends LitElement {
@property({ attribute: false })
interceptors: Interceptor[] = []

// Optional Streams factory for testing/custom backends
@property({ attribute: false })
StreamCreator?: (props: StreamsProps) => StreamsLike
Copy link
Contributor

@sourishkrout sourishkrout Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why upper-case/PascalCase?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what codex did.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should it be?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Camel-case like the others: streamCreator...


constructor() {
super()
this.#installContextBridge()
this.#contextBridge = this.#installContextBridge()
}

// Delegate theme styles to ConsoleView
Expand Down Expand Up @@ -146,6 +151,10 @@ export class RunmeConsole extends LitElement {

// Create ConsoleView element
this.consoleView = document.createElement('console-view') as ConsoleView
// Share the per-instance messaging context with the child ConsoleView
if (this.#contextBridge) {
this.consoleView.context = this.#contextBridge
}

// Set all properties on ConsoleView
this.#updateConsoleViewProperties()
Expand Down Expand Up @@ -233,14 +242,16 @@ export class RunmeConsole extends LitElement {

// Streams integration helpers
#maybeInitStreams() {
console.log('RunmeConsole: maybeInitStreams')
if (this.#streams || !this.stream) {
console.log('RunmeConsole: skipping streams init')
return
}
const knownID = this.stream.knownID ?? this.id
if (!knownID || !this.stream.runID || !this.stream.runnerEndpoint) {
throw new Error('Missing required stream properties')
}
this.#streams = new Streams({
const props: StreamsProps = {
knownID: knownID,
runID: this.stream.runID!,
sequence: this.stream.sequence ?? 0,
Expand All @@ -249,7 +260,14 @@ export class RunmeConsole extends LitElement {
autoReconnect: this.stream.reconnect,
interceptors: this.interceptors ?? [],
},
})
}
if (this.StreamCreator) {
console.log('RunmeConsole: using StreamCreator override')
this.#streams = this.StreamCreator(props)
} else {
console.log('RunmeConsole: using default Streams implementation')
this.#streams = new Streams(props)
}
const latencySub = this.#streams.connect().subscribe()
this.#streamsUnsubs.push(() => latencySub.unsubscribe())

Expand Down Expand Up @@ -413,10 +431,16 @@ export class RunmeConsole extends LitElement {
},
} as RendererContext<void>
try {
// Retain legacy behavior of setting the module-level context so
// existing consumers continue to work, but also return the instance
// bridge for per-instance use.
setContext(ctxLike)
this.consoleView && (this.consoleView.context = ctxLike)
this.#contextBridge = ctxLike
} catch {
console.error('Failed to set context bridge')
}
return ctxLike
}

// Render the UI - just render ConsoleView which handles everything
Expand Down
43 changes: 28 additions & 15 deletions packages/renderers/src/components/console/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
import { Disposable, TerminalDimensions } from 'vscode'

import { FitAddon, type ITerminalDimensions } from '../../fitAddon'
import { getContext, onClientMessage, postClientMessage } from '../../messaging'
import {
getContext,
onClientMessage,
postClientMessage,
} from '../../messaging'
import { ClientMessages, OutputType, WebViews } from '../../types'
import { ClientMessage } from '../../types'
import { closeOutput } from '../../utils'
Expand All @@ -28,6 +32,7 @@ import './open'
import './saveButton'
import './shareButton'
import { darkStyles, lightStyles } from './vscode.css'
import type { RendererContext } from 'vscode-notebook-renderer'

export interface ConsoleViewConfig {
theme: 'dark' | 'light' | 'vscode'
Expand Down Expand Up @@ -427,6 +432,14 @@ export class ConsoleView extends LitElement {
@property({ type: Boolean })
isDaggerOutput: boolean = false

// Optional per-instance messaging context; falls back to module-level context.
@property({ attribute: false })
context?: RendererContext<void>

#ctx(): RendererContext<void> {
return this.context ?? getContext()
}

protected applyThemeStyles(): void {
if (!this.shadowRoot) {
return
Expand Down Expand Up @@ -525,7 +538,7 @@ export class ConsoleView extends LitElement {
this.terminal.unicode.activeVersion = '11'
this.terminal.options.drawBoldTextInBrightColors

const ctx = getContext()
const ctx = this.#ctx()

this.disposables.push(
// todo(sebastian): what's the type of e?
Expand Down Expand Up @@ -726,7 +739,7 @@ export class ConsoleView extends LitElement {
this.#subscribeSetTerminalRows(dims)
terminalContainer.appendChild(resizeDragHandle)

const ctx = getContext()
const ctx = this.#ctx()
ctx.postMessage &&
postClientMessage(ctx, ClientMessages.terminalOpen, {
'runme.dev/id': this.id!,
Expand Down Expand Up @@ -905,7 +918,7 @@ export class ConsoleView extends LitElement {
)

const sub = debounced$.subscribe(async (terminalDimensions) => {
const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage) {
return
}
Expand All @@ -930,7 +943,7 @@ export class ConsoleView extends LitElement {
)

const sub = debounced$.subscribe(async (terminalRows) => {
const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage) {
return
}
Expand Down Expand Up @@ -963,7 +976,7 @@ export class ConsoleView extends LitElement {
this.terminal?.focus()
}

const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage) {
return
}
Expand All @@ -974,7 +987,7 @@ export class ConsoleView extends LitElement {
}

async #displayShareDialog(): Promise<boolean | void> {
const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage || !this.shareUrl) {
return
}
Expand All @@ -993,7 +1006,7 @@ export class ConsoleView extends LitElement {
}

async #triggerEscalation(): Promise<boolean | void | undefined> {
// const ctx = getContext()
// const ctx = this.#ctx()
this.isCreatingEscalation = true

// try {
Expand All @@ -1016,7 +1029,7 @@ export class ConsoleView extends LitElement {
}

async #triggerOpenEscalation(): Promise<boolean | void | undefined> {
const ctx = getContext()
const ctx = this.#ctx()

if (!this.escalationUrl) {
return
Expand All @@ -1026,7 +1039,7 @@ export class ConsoleView extends LitElement {
}

#openSessionOutput(): Promise<void | boolean> | undefined {
const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage) {
return
}
Expand All @@ -1042,7 +1055,7 @@ export class ConsoleView extends LitElement {
async #shareCellOutput(
_isUserAction: boolean
): Promise<boolean | void | undefined> {
const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage) {
return
}
Expand Down Expand Up @@ -1084,17 +1097,17 @@ export class ConsoleView extends LitElement {
}

#onWebLinkClick(_event: MouseEvent, uri: string): void {
postClientMessage(getContext(), ClientMessages.openLink, uri)
postClientMessage(this.#ctx(), ClientMessages.openLink, uri)
}

#triggerOpenCellOutput(): void {
postClientMessage(getContext(), ClientMessages.openLink, this.shareUrl!)
postClientMessage(this.#ctx(), ClientMessages.openLink, this.shareUrl!)
}

#onEscalateDisabled(): void {
const message =
'There is no Slack integration configured yet. \nOpen Dashboard to configure it'
postClientMessage(getContext(), ClientMessages.errorMessage, message)
postClientMessage(this.#ctx(), ClientMessages.errorMessage, message)
}

// Render the UI as a function of component state
Expand Down Expand Up @@ -1205,7 +1218,7 @@ export class ConsoleView extends LitElement {
}

#copy() {
const ctx = getContext()
const ctx = this.#ctx()
if (!ctx.postMessage) {
return
}
Expand Down
8 changes: 7 additions & 1 deletion packages/renderers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import './components'
import { getContext, setContext } from './messaging'
import { ClientMessages } from './types'

export { default as Streams, type Authorization } from './streams'
export {
default as Streams,
type Authorization,
type StreamsProps,
type StreamsLike,
} from './streams'
export { genRunID, Heartbeat, type StreamError } from './streams'
export { FakeStreams } from './streams/fakeStreams'

export { ConsoleView, type ConsoleViewConfig } from './components/console'
export { type RunmeConsoleStream } from './components/console/runme'
Expand Down
17 changes: 15 additions & 2 deletions packages/renderers/src/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ type Latency = {
updatedAt: bigint
}

type StreamsProps = {
export type StreamsProps = {
knownID: string
runID: string
sequence: number
Expand All @@ -92,7 +92,19 @@ type StreamsProps = {
}
}

class Streams {
export interface StreamsLike {
stdout: Observable<Uint8Array>
stderr: Observable<Uint8Array>
exitCode: Observable<number>
pid: Observable<number>
mimeType: Observable<string>
connect(): Observable<unknown>
sendExecuteRequest(executeRequest: ExecuteRequest): void
setCallback(callback: VSCodeEvent<any>): void
close(): void
}

class Streams implements StreamsLike {
private callback: VSCodeEvent<any> | undefined

private readonly queue = new Subject<pb.WebsocketRequest>()
Expand Down Expand Up @@ -179,6 +191,7 @@ class Streams {
private readonly autoReconnect: boolean

constructor({ knownID, runID, sequence, options }: StreamsProps) {
console.log("Streams: initializing for knownID", knownID, "runID", runID, "sequence", sequence )
// Set the identifiers
this.knownID = knownID
this.runID = runID
Expand Down
Loading