Merge branch 'dev' into sqlite2
This commit is contained in:
commit
8c30f551e2
20 changed files with 168 additions and 152 deletions
9
.github/workflows/deploy.yml
vendored
9
.github/workflows/deploy.yml
vendored
|
|
@ -21,6 +21,15 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
|
|
||||||
|
# Workaround for Pulumi version conflict:
|
||||||
|
# GitHub runners have Pulumi 3.212.0+ pre-installed, which removed the -root flag
|
||||||
|
# from pulumi-language-nodejs (see https://github.com/pulumi/pulumi/pull/21065).
|
||||||
|
# SST 3.17.x uses Pulumi SDK 3.210.0 which still passes -root, causing a conflict.
|
||||||
|
# Removing the system language plugin forces SST to use its bundled compatible version.
|
||||||
|
# TODO: Remove when sst supports Pulumi >3.210.0
|
||||||
|
- name: Fix Pulumi version conflict
|
||||||
|
run: sudo rm -f /usr/local/bin/pulumi-language-nodejs
|
||||||
|
|
||||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|
|
||||||
8
.github/workflows/nix-hashes.yml
vendored
8
.github/workflows/nix-hashes.yml
vendored
|
|
@ -6,13 +6,7 @@ permissions:
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
branches: [dev]
|
||||||
- "bun.lock"
|
|
||||||
- "package.json"
|
|
||||||
- "packages/*/package.json"
|
|
||||||
- "flake.lock"
|
|
||||||
- ".github/workflows/nix-hashes.yml"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
paths:
|
||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
|
|
|
||||||
2
.github/workflows/typecheck.yml
vendored
2
.github/workflows/typecheck.yml
vendored
|
|
@ -1,6 +1,8 @@
|
||||||
name: typecheck
|
name: typecheck
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [dev]
|
branches: [dev]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
- Prefer single word variable names where possible
|
- Prefer single word variable names where possible
|
||||||
- Use Bun APIs when possible, like `Bun.file()`
|
- Use Bun APIs when possible, like `Bun.file()`
|
||||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||||
|
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||||
|
|
||||||
### Naming
|
### Naming
|
||||||
|
|
||||||
|
|
|
||||||
20
bun.lock
20
bun.lock
|
|
@ -298,8 +298,8 @@
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||||
"@opentui/core": "0.1.75",
|
"@opentui/core": "0.1.76",
|
||||||
"@opentui/solid": "0.1.75",
|
"@opentui/solid": "0.1.76",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|
@ -1246,21 +1246,21 @@
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="],
|
"@opentui/core": ["@opentui/core@0.1.76", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.76", "@opentui/core-darwin-x64": "0.1.76", "@opentui/core-linux-arm64": "0.1.76", "@opentui/core-linux-x64": "0.1.76", "@opentui/core-win32-arm64": "0.1.76", "@opentui/core-win32-x64": "0.1.76", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Y4f4KH6Mbj0J6+MorcvtHSeT+Lbs3YDPEQcTRTWsPOqWz3A0F5/+OPtZKho1EtLWQqJflCWdf/JQj5A3We3qRg=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.76", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aRYNOPRKL6URovSPhRvXtBV7SqdmR7s6hmEBSdXiYo39AozTcvKviF8gJWXQATcKDEcOtRir6TsASzDq5Coheg=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.76", "", { "os": "darwin", "cpu": "x64" }, "sha512-KFaRvVQ0Wr1PgaexUkF3KYt41pYmxGJW3otENeE6WDa/nXe2AElibPFRjqSEH54YrY5Q84SDI77/wGP4LZ/Wyg=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.76", "", { "os": "linux", "cpu": "arm64" }, "sha512-s7v+GDwavfieZg8xZV4V07fXFrHfFq4UZ2JpYFDUgNs9vFp+++WUjh3pfbfE+2ldbhcG2iOtuiV9aG1tVCbTEg=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.76", "", { "os": "linux", "cpu": "x64" }, "sha512-ugwuHpmvdKRHXKVsrC3zRYY6bg2JxVCzAQ1NOiWRLq3N3N4ha6BHAkHMCeHgR/ZI4R8MSRB6vtJRVI1F9VHxjA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.76", "", { "os": "win32", "cpu": "arm64" }, "sha512-wjpRWrerPItb5E1fP4SAcNMxQp1yEukbgvP4Azip836/ixxbghL6y0P57Ya/rv7QYLrkNZXoQ+tr9oXhPH5BVA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.76", "", { "os": "win32", "cpu": "x64" }, "sha512-2YjtZJdd3iO+SY9NKocE4/Pm9VolzAthUOXjpK4Pv5pnR9hBpPvX7FFSXJTfASj7y2j1tATWrlQLocZCFP/oMA=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="],
|
"@opentui/solid": ["@opentui/solid@0.1.76", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.76", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-PiD62FGoPoVLFpY4g08i4UYlx4sGR2OmHUPj6CuZZwy2UJD4fKn1RYV+kAPHfUW4qN/88I1k/w/Dniz1WvXrAQ=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-PrEuJ7fh/rmd8ewXsnKgQ/Zu8qYsrb3D7maL9ZlVAnE=",
|
"x86_64-linux": "sha256-aRFzPzgu32XgNSk8S2z4glTlgHqEmOLZHlBQSIYIMvY=",
|
||||||
"aarch64-linux": "sha256-7+ktHTXtHMWWA9tNxX8Fb1um1JFKHQuIjIJPuveqL94=",
|
"aarch64-linux": "sha256-aCZLkmRrCa0bli0jgsaLcC5GlZdjQPbb6xD6Fc03eX8=",
|
||||||
"aarch64-darwin": "sha256-KirMU9LO7sB0ufVaWF9Y+DtxbBVyapE02GP2ytW9xLg=",
|
"aarch64-darwin": "sha256-oZOOR6k8MmabNVDQNY5ywR06rRycdnXZL+gUucKSQ+g=",
|
||||||
"x86_64-darwin": "sha256-wZJt3htmjWvwRCIoD0rkr3+8cW/xjfXfz8rdHxcFplo="
|
"x86_64-darwin": "sha256-LXIcLnjn+1eTFWIsQ9W0U2orGm59P/L470O0KFFkRHg="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||||
"@opentui/core": "0.1.75",
|
"@opentui/core": "0.1.76",
|
||||||
"@opentui/solid": "0.1.75",
|
"@opentui/solid": "0.1.76",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ function App() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
|
Clipboard.setRenderer(renderer)
|
||||||
renderer.disableStdoutInterception()
|
renderer.disableStdoutInterception()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { useSDK } from "../context/sdk"
|
||||||
import { DialogSessionRename } from "./dialog-session-rename"
|
import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
import { useKV } from "../context/kv"
|
import { useKV } from "../context/kv"
|
||||||
import { createDebouncedSignal } from "../util/signal"
|
import { createDebouncedSignal } from "../util/signal"
|
||||||
import "opentui-spinner/solid"
|
import { Spinner } from "./spinner"
|
||||||
|
|
||||||
export function DialogSessionList() {
|
export function DialogSessionList() {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
@ -32,8 +32,6 @@ export function DialogSessionList() {
|
||||||
|
|
||||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||||
|
|
||||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
||||||
|
|
||||||
const sessions = createMemo(() => searchResults() ?? sync.data.session)
|
const sessions = createMemo(() => searchResults() ?? sync.data.session)
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
|
|
@ -56,11 +54,7 @@ export function DialogSessionList() {
|
||||||
value: x.id,
|
value: x.id,
|
||||||
category,
|
category,
|
||||||
footer: Locale.time(x.time.updated),
|
footer: Locale.time(x.time.updated),
|
||||||
gutter: isWorking ? (
|
gutter: isWorking ? <Spinner /> : undefined,
|
||||||
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
|
||||||
<spinner frames={spinnerFrames} interval={80} color={theme.primary} />
|
|
||||||
</Show>
|
|
||||||
) : undefined,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
24
packages/opencode/src/cli/cmd/tui/component/spinner.tsx
Normal file
24
packages/opencode/src/cli/cmd/tui/component/spinner.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Show } from "solid-js"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { useKV } from "../context/kv"
|
||||||
|
import type { JSX } from "@opentui/solid"
|
||||||
|
import type { RGBA } from "@opentui/core"
|
||||||
|
import "opentui-spinner/solid"
|
||||||
|
|
||||||
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||||
|
|
||||||
|
export function Spinner(props: { children?: JSX.Element; color?: RGBA }) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const kv = useKV()
|
||||||
|
const color = () => props.color ?? theme.textMuted
|
||||||
|
return (
|
||||||
|
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={color()}>⋯ {props.children}</text>}>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<spinner frames={frames} interval={80} color={color()} />
|
||||||
|
<Show when={props.children}>
|
||||||
|
<text fg={color()}>{props.children}</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import path from "path"
|
||||||
import { useRoute, useRouteData } from "@tui/context/route"
|
import { useRoute, useRouteData } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
import { SplitBorder } from "@tui/component/border"
|
||||||
|
import { Spinner } from "@tui/component/spinner"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import {
|
import {
|
||||||
BoxRenderable,
|
BoxRenderable,
|
||||||
|
|
@ -1559,7 +1560,13 @@ function InlineTool(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlockTool(props: { title: string; children: JSX.Element; onClick?: () => void; part?: ToolPart }) {
|
function BlockTool(props: {
|
||||||
|
title: string
|
||||||
|
children: JSX.Element
|
||||||
|
onClick?: () => void
|
||||||
|
part?: ToolPart
|
||||||
|
spinner?: boolean
|
||||||
|
}) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const [hover, setHover] = createSignal(false)
|
const [hover, setHover] = createSignal(false)
|
||||||
|
|
@ -1582,9 +1589,16 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () =
|
||||||
props.onClick?.()
|
props.onClick?.()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text paddingLeft={3} fg={theme.textMuted}>
|
<Show
|
||||||
{props.title}
|
when={props.spinner}
|
||||||
</text>
|
fallback={
|
||||||
|
<text paddingLeft={3} fg={theme.textMuted}>
|
||||||
|
{props.title}
|
||||||
|
</text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
|
||||||
|
</Show>
|
||||||
{props.children}
|
{props.children}
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<text fg={theme.error}>{error()}</text>
|
<text fg={theme.error}>{error()}</text>
|
||||||
|
|
@ -1799,9 +1813,21 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
const { navigate } = useRoute()
|
const { navigate } = useRoute()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
const sync = useSync()
|
||||||
|
|
||||||
const current = createMemo(() => props.metadata.summary?.findLast((x) => x.state.status !== "pending"))
|
const tools = createMemo(() => {
|
||||||
const color = createMemo(() => local.agent.color(props.input.subagent_type ?? "unknown"))
|
const sessionID = props.metadata.sessionId
|
||||||
|
const msgs = sync.data.message[sessionID ?? ""] ?? []
|
||||||
|
return msgs.flatMap((msg) =>
|
||||||
|
(sync.data.part[msg.id] ?? [])
|
||||||
|
.filter((part): part is ToolPart => part.type === "tool")
|
||||||
|
.map((part) => ({ tool: part.tool, state: part.state })),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
|
||||||
|
|
||||||
|
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
@ -1814,16 +1840,21 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
part={props.part}
|
part={props.part}
|
||||||
|
spinner={isRunning()}
|
||||||
>
|
>
|
||||||
<box>
|
<box>
|
||||||
<text style={{ fg: theme.textMuted }}>
|
<text style={{ fg: theme.textMuted }}>
|
||||||
{props.input.description} ({props.metadata.summary?.length ?? 0} toolcalls)
|
{props.input.description} ({tools().length} toolcalls)
|
||||||
</text>
|
</text>
|
||||||
<Show when={current()}>
|
<Show when={current()}>
|
||||||
<text style={{ fg: current()!.state.status === "error" ? theme.error : theme.textMuted }}>
|
{(item) => {
|
||||||
└ {Locale.titlecase(current()!.tool)}{" "}
|
const title = item().state.status === "completed" ? (item().state as any).title : ""
|
||||||
{current()!.state.status === "completed" ? current()!.state.title : ""}
|
return (
|
||||||
</text>
|
<text style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
|
||||||
|
└ {Locale.titlecase(item().tool)} {title}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
<Show when={props.metadata.sessionId}>
|
<Show when={props.metadata.sessionId}>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,12 @@
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import type { CliRenderer } from "@opentui/core"
|
||||||
import { platform, release } from "os"
|
import { platform, release } from "os"
|
||||||
import clipboardy from "clipboardy"
|
import clipboardy from "clipboardy"
|
||||||
import { lazy } from "../../../../util/lazy.js"
|
import { lazy } from "../../../../util/lazy.js"
|
||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
/**
|
const rendererRef = { current: undefined as CliRenderer | undefined }
|
||||||
* Writes text to clipboard via OSC 52 escape sequence.
|
|
||||||
* This allows clipboard operations to work over SSH by having
|
|
||||||
* the terminal emulator handle the clipboard locally.
|
|
||||||
*/
|
|
||||||
function writeOsc52(text: string): void {
|
|
||||||
if (!process.stdout.isTTY) return
|
|
||||||
const base64 = Buffer.from(text).toString("base64")
|
|
||||||
const osc52 = `\x1b]52;c;${base64}\x07`
|
|
||||||
// tmux and screen require DCS passthrough wrapping
|
|
||||||
const passthrough = process.env["TMUX"] || process.env["STY"]
|
|
||||||
const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
|
|
||||||
process.stdout.write(sequence)
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Clipboard {
|
export namespace Clipboard {
|
||||||
export interface Content {
|
export interface Content {
|
||||||
|
|
@ -26,6 +14,10 @@ export namespace Clipboard {
|
||||||
mime: string
|
mime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setRenderer(renderer: CliRenderer | undefined): void {
|
||||||
|
rendererRef.current = renderer
|
||||||
|
}
|
||||||
|
|
||||||
export async function read(): Promise<Content | undefined> {
|
export async function read(): Promise<Content | undefined> {
|
||||||
const os = platform()
|
const os = platform()
|
||||||
|
|
||||||
|
|
@ -154,7 +146,11 @@ export namespace Clipboard {
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function copy(text: string): Promise<void> {
|
export async function copy(text: string): Promise<void> {
|
||||||
writeOsc52(text)
|
const renderer = rendererRef.current
|
||||||
|
if (renderer) {
|
||||||
|
const copied = renderer.copyToClipboardOSC52(text)
|
||||||
|
if (copied) return
|
||||||
|
}
|
||||||
await getCopyMethod()(text)
|
await getCopyMethod()(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,100 +275,56 @@ export namespace Ripgrep {
|
||||||
log.info("tree", input)
|
log.info("tree", input)
|
||||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd, signal: input.signal }))
|
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd, signal: input.signal }))
|
||||||
interface Node {
|
interface Node {
|
||||||
path: string[]
|
name: string
|
||||||
children: Node[]
|
children: Map<string, Node>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPath(node: Node, parts: string[], create: boolean) {
|
function dir(node: Node, name: string) {
|
||||||
if (parts.length === 0) return node
|
const existing = node.children.get(name)
|
||||||
let current = node
|
if (existing) return existing
|
||||||
for (const part of parts) {
|
const next = { name, children: new Map() }
|
||||||
let existing = current.children.find((x) => x.path.at(-1) === part)
|
node.children.set(name, next)
|
||||||
if (!existing) {
|
return next
|
||||||
if (!create) return
|
|
||||||
existing = {
|
|
||||||
path: current.path.concat(part),
|
|
||||||
children: [],
|
|
||||||
}
|
|
||||||
current.children.push(existing)
|
|
||||||
}
|
|
||||||
current = existing
|
|
||||||
}
|
|
||||||
return current
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const root: Node = {
|
const root: Node = { name: "", children: new Map() }
|
||||||
path: [],
|
|
||||||
children: [],
|
|
||||||
}
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.includes(".opencode")) continue
|
if (file.includes(".opencode")) continue
|
||||||
const parts = file.split(path.sep)
|
const parts = file.split(path.sep)
|
||||||
getPath(root, parts, true)
|
if (parts.length < 2) continue
|
||||||
}
|
let node = root
|
||||||
|
for (const part of parts.slice(0, -1)) {
|
||||||
function sort(node: Node) {
|
node = dir(node, part)
|
||||||
node.children.sort((a, b) => {
|
|
||||||
if (!a.children.length && b.children.length) return 1
|
|
||||||
if (!b.children.length && a.children.length) return -1
|
|
||||||
return a.path.at(-1)!.localeCompare(b.path.at(-1)!)
|
|
||||||
})
|
|
||||||
for (const child of node.children) {
|
|
||||||
sort(child)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort(root)
|
|
||||||
|
|
||||||
let current = [root]
|
function count(node: Node): number {
|
||||||
const result: Node = {
|
let total = 0
|
||||||
path: [],
|
for (const child of node.children.values()) {
|
||||||
children: [],
|
total += 1 + count(child)
|
||||||
}
|
}
|
||||||
|
return total
|
||||||
let processed = 0
|
|
||||||
const limit = input.limit ?? 50
|
|
||||||
while (current.length > 0) {
|
|
||||||
const next = []
|
|
||||||
for (const node of current) {
|
|
||||||
if (node.children.length) next.push(...node.children)
|
|
||||||
}
|
|
||||||
const max = Math.max(...current.map((x) => x.children.length))
|
|
||||||
for (let i = 0; i < max && processed < limit; i++) {
|
|
||||||
for (const node of current) {
|
|
||||||
const child = node.children[i]
|
|
||||||
if (!child) continue
|
|
||||||
getPath(result, child.path, true)
|
|
||||||
processed++
|
|
||||||
if (processed >= limit) break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (processed >= limit) {
|
|
||||||
for (const node of [...current, ...next]) {
|
|
||||||
const compare = getPath(result, node.path, false)
|
|
||||||
if (!compare) continue
|
|
||||||
if (compare?.children.length !== node.children.length) {
|
|
||||||
const diff = node.children.length - compare.children.length
|
|
||||||
compare.children.push({
|
|
||||||
path: compare.path.concat(`[${diff} truncated]`),
|
|
||||||
children: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
current = next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const total = count(root)
|
||||||
|
const limit = input.limit ?? total
|
||||||
const lines: string[] = []
|
const lines: string[] = []
|
||||||
|
const queue: { node: Node; path: string }[] = []
|
||||||
|
for (const child of Array.from(root.children.values()).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||||
|
queue.push({ node: child, path: child.name })
|
||||||
|
}
|
||||||
|
|
||||||
function render(node: Node, depth: number) {
|
let used = 0
|
||||||
const indent = "\t".repeat(depth)
|
for (let i = 0; i < queue.length && used < limit; i++) {
|
||||||
lines.push(indent + node.path.at(-1) + (node.children.length ? "/" : ""))
|
const { node, path } = queue[i]
|
||||||
for (const child of node.children) {
|
lines.push(path)
|
||||||
render(child, depth + 1)
|
used++
|
||||||
|
for (const child of Array.from(node.children.values()).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||||
|
queue.push({ node: child, path: `${path}/${child.name}` })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.children.map((x) => render(x, 0))
|
|
||||||
|
if (total > used) lines.push(`[${total - used} truncated]`)
|
||||||
|
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,7 @@ export namespace SessionProcessor {
|
||||||
sessionID: input.assistantMessage.sessionID,
|
sessionID: input.assistantMessage.sessionID,
|
||||||
error: input.assistantMessage.error,
|
error: input.assistantMessage.error,
|
||||||
})
|
})
|
||||||
|
SessionStatus.set(input.sessionID, { type: "idle" })
|
||||||
}
|
}
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
const patch = await Snapshot.patch(snapshot)
|
const patch = await Snapshot.patch(snapshot)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export namespace SessionPrompt {
|
||||||
abort: AbortController
|
abort: AbortController
|
||||||
callbacks: {
|
callbacks: {
|
||||||
resolve(input: MessageV2.WithParts): void
|
resolve(input: MessageV2.WithParts): void
|
||||||
reject(): void
|
reject(reason?: any): void
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
> = {}
|
> = {}
|
||||||
|
|
@ -72,7 +72,7 @@ export namespace SessionPrompt {
|
||||||
for (const item of Object.values(current)) {
|
for (const item of Object.values(current)) {
|
||||||
item.abort.abort()
|
item.abort.abort()
|
||||||
for (const callback of item.callbacks) {
|
for (const callback of item.callbacks) {
|
||||||
callback.reject()
|
callback.reject(new DOMException("Aborted", "AbortError"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -249,10 +249,13 @@ export namespace SessionPrompt {
|
||||||
log.info("cancel", { sessionID })
|
log.info("cancel", { sessionID })
|
||||||
const s = state()
|
const s = state()
|
||||||
const match = s[sessionID]
|
const match = s[sessionID]
|
||||||
if (!match) return
|
if (!match) {
|
||||||
|
SessionStatus.set(sessionID, { type: "idle" })
|
||||||
|
return
|
||||||
|
}
|
||||||
match.abort.abort()
|
match.abort.abort()
|
||||||
for (const item of match.callbacks) {
|
for (const item of match.callbacks) {
|
||||||
item.reject()
|
item.reject(new DOMException("Aborted", "AbortError"))
|
||||||
}
|
}
|
||||||
delete s[sessionID]
|
delete s[sessionID]
|
||||||
SessionStatus.set(sessionID, { type: "idle" })
|
SessionStatus.set(sessionID, { type: "idle" })
|
||||||
|
|
|
||||||
|
|
@ -36,16 +36,16 @@ export namespace SystemPrompt {
|
||||||
` Platform: ${process.platform}`,
|
` Platform: ${process.platform}`,
|
||||||
` Today's date: ${new Date().toDateString()}`,
|
` Today's date: ${new Date().toDateString()}`,
|
||||||
`</env>`,
|
`</env>`,
|
||||||
`<files>`,
|
`<directories>`,
|
||||||
` ${
|
` ${
|
||||||
project.vcs === "git" && false
|
project.vcs === "git" && false
|
||||||
? await Ripgrep.tree({
|
? await Ripgrep.tree({
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
limit: 200,
|
limit: 50,
|
||||||
})
|
})
|
||||||
: ""
|
: ""
|
||||||
}`,
|
}`,
|
||||||
`</files>`,
|
`</directories>`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,6 @@ export const TaskTool = Tool.define("task", async (ctx) => {
|
||||||
ctx.metadata({
|
ctx.metadata({
|
||||||
title: params.description,
|
title: params.description,
|
||||||
metadata: {
|
metadata: {
|
||||||
summary: Object.values(parts).sort((a, b) => a.id.localeCompare(b.id)),
|
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
model,
|
model,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ test("unicode filenames", async () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("unicode filenames modification and restore", async () => {
|
test.skip("unicode filenames modification and restore", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,8 @@
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": "./src/index.ts",
|
||||||
"types": "./dist/index.d.ts",
|
"./tool": "./src/tool.ts"
|
||||||
"import": "./dist/index.js"
|
|
||||||
},
|
|
||||||
"./tool": {
|
|
||||||
"types": "./dist/tool.d.ts",
|
|
||||||
"import": "./dist/tool.js"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,18 @@ async function main() {
|
||||||
throw new Error(`${failed.length} PR(s) failed to merge`)
|
throw new Error(`${failed.length} PR(s) failed to merge`)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("\nForce pushing beta branch...")
|
console.log("\nChecking if beta branch has changes...")
|
||||||
|
await $`git fetch origin beta`
|
||||||
|
|
||||||
|
const localTree = await $`git rev-parse beta^{tree}`.text()
|
||||||
|
const remoteTree = await $`git rev-parse origin/beta^{tree}`.text()
|
||||||
|
|
||||||
|
if (localTree.trim() === remoteTree.trim()) {
|
||||||
|
console.log("Beta branch has identical contents, no push needed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Force pushing beta branch...")
|
||||||
await $`git push origin beta --force --no-verify`
|
await $`git push origin beta --force --no-verify`
|
||||||
|
|
||||||
console.log("Successfully synced beta branch")
|
console.log("Successfully synced beta branch")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue