Compare commits

..

2 Commits

Author SHA1 Message Date
Dax Raad 939c2c2411 fix(core): list default agent first 2026-08-13 04:56:23 +00:00
Kit Langton 56973e0ca4 fix(core): reject inherited workspace providers (#42227) 2026-08-12 22:55:32 -04:00
6 changed files with 40 additions and 68 deletions
+4 -1
View File
@@ -122,7 +122,10 @@ const layer = Layer.effect(
return { id: info?.id ?? defaultID, info }
}),
list: Effect.fn("Agent.list")(function* () {
return Array.fromIterable(state.get().agents.values())
const agents = Array.fromIterable(state.get().agents.values())
const selected = selectedDefault()
if (!selected) return agents
return [selected, ...agents.filter((agent) => agent.id !== selected.id)]
}),
})
}),
+1 -1
View File
@@ -55,7 +55,7 @@ export class RegistryService extends Context.Service<RegistryService, Registry>(
export const registry = (drivers: Readonly<Record<string, Interface>>): Registry => ({
get: (provider) => {
const driver = drivers[provider]
const driver = Object.hasOwn(drivers, provider) ? drivers[provider] : undefined
return driver ? Effect.succeed(driver) : Effect.fail(new ProviderNotFound({ provider }))
},
})
+20
View File
@@ -68,6 +68,26 @@ describe("Agent", () => {
}),
)
it.effect("lists the selected default agent first", () =>
Effect.gen(function* () {
const agent = yield* Agent.Service
yield* agent.transform((editor) => {
editor.update(Agent.ID.make("build"), (info) => {
info.mode = "primary"
})
editor.update(Agent.ID.make("reviewer"), (info) => {
info.mode = "primary"
})
editor.update(Agent.ID.make("explore"), (info) => {
info.mode = "subagent"
})
editor.default(Agent.ID.make("reviewer"))
})
expect((yield* agent.list()).map((info) => String(info.id))).toEqual(["reviewer", "build", "explore"])
}),
)
it.effect("rebuilds state when a transform is replaced", () =>
Effect.gen(function* () {
const agent = yield* Agent.Service
+12
View File
@@ -48,6 +48,18 @@ beforeEach(() => {
failConnect = false
})
it.effect("rejects unregistered workspace providers", () =>
Effect.gen(function* () {
const registry = WorkspaceDriver.registry({ fake: driver })
for (const provider of ["missing", "constructor", "toString", "__proto__"]) {
expect(yield* registry.get(provider).pipe(Effect.flip)).toEqual(
new WorkspaceDriver.ProviderNotFound({ provider }),
)
}
}),
)
it.effect("persists the workspace lifecycle and reconnects after idle suspension", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
@@ -5,7 +5,6 @@ import {
MouseEvent,
PasteEvent,
decodePasteBytes,
getTreeSitterClient,
type ColorInput,
type KeyEvent,
} from "@opentui/core"
@@ -99,7 +98,6 @@ export type PromptRef = {
}
const DRAFT_RETENTION_MIN_CHARS = 20
const SHELL_SYNTAX_HIGHLIGHT_REF = 65_535
function randomIndex(count: number) {
if (count <= 0) return 0
@@ -342,47 +340,6 @@ export function Prompt(props: PromptProps) {
})
let disposed = false
let pasteQueue = Promise.resolve()
let syntaxHighlightVersion = 0
createEffect(() => {
const mode = store.mode
const content = store.prompt.text
const style = syntax()
const version = ++syntaxHighlightVersion
if (input && !input.isDestroyed) input.editBuffer.removeHighlightsByRef(SHELL_SYNTAX_HIGHLIGHT_REF)
if (mode !== "shell" || !content || !input || input.isDestroyed) return
const timeout = setTimeout(() => {
getTreeSitterClient()
.highlightOnce(content, "bash")
.then((result) => {
if (version !== syntaxHighlightVersion || !result.highlights || !input || input.isDestroyed) return
const bytes = new TextEncoder().encode(content)
const decoder = new TextDecoder()
result.highlights.forEach(([start, end, group]) => {
const styleId = style.getStyleId(group)
if (styleId === null) return
const before = decoder.decode(bytes.subarray(0, start))
const highlighted = decoder.decode(bytes.subarray(start, end))
input.editBuffer.addHighlightByCharRange({
start: promptOffsetWidth(before) - (before.match(/\n/g)?.length ?? 0),
end:
promptOffsetWidth(before) +
promptOffsetWidth(highlighted) -
((before + highlighted).match(/\n/g)?.length ?? 0),
styleId,
priority: 0,
hlRef: SHELL_SYNTAX_HIGHLIGHT_REF,
})
})
renderer.requestRender()
})
.catch(() => {})
}, 50)
onCleanup(() => clearTimeout(timeout))
})
function enqueuePaste(run: (changed: () => boolean) => Promise<void>) {
pasteQueue = pasteQueue
+3 -23
View File
@@ -1938,7 +1938,6 @@ function RevertMessage(props: {
function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "shell" }> }) {
const theme = useTheme("elevated")
const { currentSyntax: syntax } = useThemes()
const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
return (
@@ -1953,16 +1952,7 @@ function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "she
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background.default}
>
<box flexDirection="row" gap={1}>
<text fg={theme.text.default}>$</text>
<code
conceal={false}
fg={theme.text.default}
filetype="bash"
syntaxStyle={syntax()}
content={props.message.command}
/>
</box>
<text fg={theme.text.default}>$ {props.message.command}</text>
<Show when={output()}>
<text fg={theme.text.subdued}>{output()}</text>
</Show>
@@ -2794,7 +2784,6 @@ const SHELL_DISPLAY_LIMIT = 1024 * 1024
function Shell(props: ToolProps) {
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
const ctx = use()
const client = useClient()
const data = useData()
@@ -2926,20 +2915,11 @@ function Shell(props: ToolProps) {
fallback={
<box flexDirection="row" gap={1}>
<text fg={theme.text.default}>{prompt()}</text>
<code
conceal={false}
fg={theme.text.default}
filetype="bash"
syntaxStyle={syntax()}
content={limitedInput()}
/>
<text fg={theme.text.default}>{limitedInput()}</text>
</box>
}
>
<box flexDirection="row" gap={1}>
<Spinner color={color()} />
<code conceal={false} fg={color()} filetype="bash" syntaxStyle={syntax()} content={limitedInput()} />
</box>
<Spinner color={color()}>{limitedInput()}</Spinner>
</Show>
<Show when={limitedOutput()}>
<text fg={theme.text.subdued}>{limitedOutput()}</text>