mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 22:21:25 -04:00
Compare commits
3 Commits
tab-playground
...
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 12a931a220 | |||
| 90100c1365 | |||
| 139c9febe4 |
@@ -94,9 +94,9 @@ export const settings: Setting[] = [
|
||||
keywords: ["transcript", "messages"],
|
||||
},
|
||||
{
|
||||
title: "Tabs",
|
||||
category: "Session",
|
||||
path: ["session", "tabs"],
|
||||
title: "Enabled",
|
||||
category: "Tabs",
|
||||
path: ["tabs", "enabled"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
|
||||
@@ -120,11 +120,15 @@ export const Info = Schema.Struct({
|
||||
markdown: Schema.optional(Schema.Literals(["source", "rendered"])).annotate({
|
||||
description: "Show Markdown syntax markers or conceal them in rendered transcript content",
|
||||
}),
|
||||
tabs: Schema.optional(Schema.Boolean).annotate({
|
||||
}),
|
||||
).annotate({ description: "Session transcript presentation settings" }),
|
||||
tabs: Schema.optional(
|
||||
Schema.Struct({
|
||||
enabled: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Use a persistent session tab strip instead of pinned quick-switch sessions",
|
||||
}),
|
||||
}),
|
||||
).annotate({ description: "Session transcript presentation settings" }),
|
||||
).annotate({ description: "Session tab settings" }),
|
||||
mini: Schema.optional(
|
||||
Schema.Struct({
|
||||
thinking: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
||||
|
||||
@@ -34,7 +34,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||
const event = useEvent()
|
||||
const config = useConfig().data
|
||||
const filePath = path.join(useTuiPaths().state, "session-tabs.json")
|
||||
const enabled = () => config.session?.tabs ?? false
|
||||
const enabled = () => config.tabs?.enabled ?? false
|
||||
const state: {
|
||||
pending: boolean
|
||||
saving: boolean
|
||||
|
||||
@@ -767,19 +767,23 @@ export function RunSubagentSelectBody(props: {
|
||||
onRows?: (rows: number) => void
|
||||
mono?: boolean
|
||||
}) {
|
||||
const [active, setActive] = createSignal(true)
|
||||
const entries = createMemo<SubagentEntry[]>(() =>
|
||||
props.tabs().map((item) => {
|
||||
const title = item.description || item.title || item.label
|
||||
return {
|
||||
category: "",
|
||||
display: title,
|
||||
description: title === item.label ? undefined : item.label,
|
||||
footer: subagentStatusLabel(item.status),
|
||||
keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`,
|
||||
sessionID: item.sessionID,
|
||||
current: props.current() === item.sessionID,
|
||||
}
|
||||
}),
|
||||
props
|
||||
.tabs()
|
||||
.filter((item) => (active() ? item.status === "running" : item.status !== "running"))
|
||||
.map((item) => {
|
||||
const title = item.description || item.title || item.label
|
||||
return {
|
||||
category: "",
|
||||
display: title,
|
||||
description: title === item.label ? undefined : item.label,
|
||||
footer: subagentStatusLabel(item.status),
|
||||
keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`,
|
||||
sessionID: item.sessionID,
|
||||
current: props.current() === item.sessionID,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const controller = createSearchablePanelController({
|
||||
entries,
|
||||
@@ -788,6 +792,12 @@ export function RunSubagentSelectBody(props: {
|
||||
onSelect: (item) => props.onSelect(item.sessionID),
|
||||
isCurrent: (item) => item.current,
|
||||
closeOnFirstUp: true,
|
||||
onKey(event) {
|
||||
if (event.name.toLowerCase() !== "tab") return false
|
||||
event.preventDefault()
|
||||
setActive((value) => !value)
|
||||
return true
|
||||
},
|
||||
onRows: props.onRows,
|
||||
})
|
||||
|
||||
@@ -801,6 +811,7 @@ export function RunSubagentSelectBody(props: {
|
||||
theme={props.theme}
|
||||
inputRef={controller.inputRef}
|
||||
onQuery={controller.setQuery}
|
||||
hint={`tab show ${active() ? "inactive" : "active"}`}
|
||||
mono={props.mono}
|
||||
>
|
||||
<RunFooterMenu
|
||||
|
||||
@@ -27,6 +27,7 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
|
||||
const session = createMemo(() => data.session.get(props.sessionID))
|
||||
const [store, setStore] = createStore({ selected: 0, active: true })
|
||||
|
||||
const entries = createMemo<SubagentEntry[]>(() => {
|
||||
const current = session()
|
||||
@@ -72,10 +73,9 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return result.filter((entry) => (store.active ? entry.status === "running" : entry.status !== "running"))
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({ selected: 0 })
|
||||
let selectedSessionID = ""
|
||||
let wasActive = false
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
@@ -90,7 +90,7 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
if (!active) {
|
||||
if (wasActive) {
|
||||
selectedSessionID = ""
|
||||
setStore("selected", 0)
|
||||
setStore({ selected: 0, active: true })
|
||||
}
|
||||
wasActive = false
|
||||
return
|
||||
@@ -140,8 +140,15 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
label: "Subagents",
|
||||
hints: () => {
|
||||
const entry = selectedEntry()
|
||||
if (!entry || entry.status !== "running") return []
|
||||
return [{ label: "interrupt", shortcut: shortcuts.get("composer.subagent.interrupt") ?? "" }]
|
||||
return [
|
||||
...(entry?.status === "running"
|
||||
? [{ label: "interrupt", shortcut: shortcuts.get("composer.subagent.interrupt") ?? "" }]
|
||||
: []),
|
||||
{
|
||||
label: `show ${store.active ? "inactive" : "active"}`,
|
||||
shortcut: shortcuts.get("composer.subagent.toggle-activity") ?? "",
|
||||
},
|
||||
]
|
||||
},
|
||||
onClose: () => {
|
||||
const parentID = session()?.parentID
|
||||
@@ -189,6 +196,16 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
if (entry) navigate({ type: "session", sessionID: entry.sessionID })
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "composer.subagent.toggle-activity",
|
||||
title: "Toggle active subagents",
|
||||
group: "Composer",
|
||||
bind: "ctrl+a",
|
||||
run() {
|
||||
setStore({ selected: 0, active: !store.active })
|
||||
scroll?.scrollTo(0)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "composer.subagent.interrupt",
|
||||
title: "Interrupt subagent",
|
||||
@@ -206,7 +223,10 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
return (
|
||||
<Show when={composer.active("subagents")}>
|
||||
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
|
||||
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No subagents</text>}>
|
||||
<Show
|
||||
when={entries().length > 0}
|
||||
fallback={<text fg={theme.text.subdued}> No {store.active ? "active" : "inactive"} subagents</text>}
|
||||
>
|
||||
<For each={entries()}>
|
||||
{(entry, index) => {
|
||||
const active = createMemo(() => index() === selected())
|
||||
|
||||
@@ -17,8 +17,8 @@ test("validates mini replay settings", () => {
|
||||
test("validates the session tabs setting", () => {
|
||||
const decode = Schema.decodeUnknownSync(Info)
|
||||
|
||||
expect(decode({ session: { tabs: true } })).toEqual({ session: { tabs: true } })
|
||||
expect(() => decode({ session: { tabs: "on" } })).toThrow()
|
||||
expect(decode({ tabs: { enabled: true } })).toEqual({ tabs: { enabled: true } })
|
||||
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
|
||||
})
|
||||
|
||||
test("resolves nested config and keybind defaults", () => {
|
||||
|
||||
@@ -820,7 +820,7 @@ test("direct command panel keeps completed subagents available", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("direct subagent panel renders active subagents", async () => {
|
||||
test("direct subagent panel toggles between active and inactive subagents", async () => {
|
||||
const [tabs] = createSignal([
|
||||
subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" }),
|
||||
subagent({ sessionID: "s-2", label: "General", description: "Write migration plan", status: "completed" }),
|
||||
@@ -856,12 +856,22 @@ test("direct subagent panel renders active subagents", async () => {
|
||||
|
||||
expect(frame).toContain("Select subagent")
|
||||
expect(frame).toContain("Inspect auth flow")
|
||||
expect(frame).toContain("Write migration plan")
|
||||
expect(frame).toContain("done")
|
||||
expect(frame).not.toContain("Write migration plan")
|
||||
expect(frame).not.toContain("done")
|
||||
expect(frame).toContain("tab show inactive")
|
||||
expect(frame).not.toContain("┌")
|
||||
expect(frame).not.toContain("┃")
|
||||
expectPaletteList(list, 0)
|
||||
expect(rows).toBe(8)
|
||||
expect(rows).toBe(7)
|
||||
|
||||
app.mockInput.pressKey("TAB")
|
||||
await app.renderOnce()
|
||||
const inactive = app.captureCharFrame()
|
||||
|
||||
expect(inactive).not.toContain("Inspect auth flow")
|
||||
expect(inactive).toContain("Write migration plan")
|
||||
expect(inactive).toContain("done")
|
||||
expect(inactive).toContain("tab show active")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
import config from "../../../config.mjs"
|
||||
export const console = config.console
|
||||
|
||||
:::note
|
||||
OpenCode 1 installs and runs as `opencode`. OpenCode 2 installs separately as `opencode2`, so you can keep both versions
|
||||
installed and run them side by side. See the [OpenCode 2 docs](https://opencode.ai/v2/docs/) to install V2.
|
||||
:::
|
||||
|
||||
[**OpenCode**](/) is an open source AI coding agent. It's available as a terminal-based interface, desktop app, or IDE extension.
|
||||
|
||||

|
||||
@@ -31,7 +36,8 @@ To use OpenCode in your terminal, you'll need:
|
||||
|
||||
## Install
|
||||
|
||||
The easiest way to install OpenCode is through the install script.
|
||||
The easiest way to install OpenCode 1 is through the install script. These installation methods provide the `opencode`
|
||||
binary and do not replace an `opencode2` installation.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
@@ -8,6 +8,11 @@ description: "Get started with OpenCode."
|
||||
wipe your data, things may break, and APIs, configuration, and plugin APIs may change.
|
||||
</Callout>
|
||||
|
||||
<Callout type="note">
|
||||
OpenCode 2 installs and runs as `opencode2`. It does not replace OpenCode 1's `opencode` binary, so you can keep both
|
||||
versions installed and run them side by side.
|
||||
</Callout>
|
||||
|
||||
## Install
|
||||
|
||||
<Callout type="note">The curl install script is not available in beta.</Callout>
|
||||
@@ -37,10 +42,8 @@ You can also install it with the following package managers.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
The package uses a trusted postinstall script to select the native binary for your platform. The Bun and pnpm commands
|
||||
above explicitly allow that script to run.
|
||||
|
||||
<Callout type="note">During beta, the binary is called `opencode2`.</Callout>
|
||||
The package uses a trusted postinstall script to select the native `opencode2` binary for your platform. The Bun and pnpm
|
||||
commands above explicitly allow that script to run.
|
||||
|
||||
### Homebrew
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ title: "Migrate from V1"
|
||||
description: "Move from OpenCode V1 to the OpenCode 2.0 beta."
|
||||
---
|
||||
|
||||
<Callout type="note">
|
||||
OpenCode 1 and OpenCode 2 can be installed side by side. V1 runs as `opencode`, while V2 installs and runs separately as
|
||||
`opencode2`.
|
||||
</Callout>
|
||||
|
||||
## Breaking changes
|
||||
|
||||
V2 has three intentional breaking changes:
|
||||
@@ -27,9 +32,6 @@ an expected migration requirement.
|
||||
continue to change.
|
||||
</Callout>
|
||||
|
||||
During the beta, OpenCode V1 and V2 use different executable names. You can keep using `opencode` for V1 while trying V2
|
||||
with `opencode2`.
|
||||
|
||||
## Install the beta
|
||||
|
||||
Install the beta from the `next` distribution tag:
|
||||
|
||||
Reference in New Issue
Block a user