Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8825cd3811 | ||
|
|
3d9a5d9970 | ||
|
|
1f9e195fa6 | ||
|
|
73c012c76c | ||
|
|
2ace57404b | ||
|
|
8c4b5e088b |
@@ -57,6 +57,7 @@ export namespace LSPServer {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
if (!bin) {
|
||||
if (!Bun.which("go")) return
|
||||
log.info("installing gopls")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
|
||||
@@ -67,9 +67,12 @@ export namespace Share {
|
||||
}
|
||||
|
||||
export async function remove(id: string) {
|
||||
const share = await Session.getShare(id).catch(() => {})
|
||||
if (!share) return
|
||||
const { secret } = share
|
||||
return fetch(`${URL}/share_delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
body: JSON.stringify({ id, secret }),
|
||||
}).then((x) => x.json())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ const (
|
||||
SessionNewCommand CommandName = "session_new"
|
||||
SessionListCommand CommandName = "session_list"
|
||||
SessionShareCommand CommandName = "session_share"
|
||||
SessionUnshareCommand CommandName = "session_unshare"
|
||||
SessionInterruptCommand CommandName = "session_interrupt"
|
||||
SessionCompactCommand CommandName = "session_compact"
|
||||
ToolDetailsCommand CommandName = "tool_details"
|
||||
@@ -160,6 +161,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Keybindings: parseBindings("<leader>s"),
|
||||
Trigger: "share",
|
||||
},
|
||||
{
|
||||
Name: SessionUnshareCommand,
|
||||
Description: "unshare session",
|
||||
Keybindings: parseBindings("<leader>u"),
|
||||
Trigger: "unshare",
|
||||
},
|
||||
{
|
||||
Name: SessionInterruptCommand,
|
||||
Description: "interrupt session",
|
||||
@@ -289,7 +296,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
{
|
||||
Name: MessagesRevertCommand,
|
||||
Description: "revert message",
|
||||
Keybindings: parseBindings("<leader>u"),
|
||||
Keybindings: parseBindings("<leader>r"),
|
||||
},
|
||||
{
|
||||
Name: AppExitCommand,
|
||||
|
||||
@@ -162,18 +162,16 @@ func renderContentBlock(
|
||||
|
||||
if highlight {
|
||||
style = style.
|
||||
BorderLeftBackground(t.Primary()).
|
||||
BorderLeftForeground(t.Primary()).
|
||||
BorderRightForeground(t.Primary()).
|
||||
BorderRightBackground(t.Primary())
|
||||
BorderLeftForeground(borderColor).
|
||||
BorderRightForeground(borderColor)
|
||||
}
|
||||
}
|
||||
|
||||
if highlight {
|
||||
style = style.
|
||||
Foreground(t.Text()).
|
||||
Bold(true).
|
||||
Background(t.BackgroundElement())
|
||||
Background(t.BackgroundElement()).
|
||||
Bold(true)
|
||||
}
|
||||
|
||||
content = style.Render(content)
|
||||
@@ -211,7 +209,7 @@ func renderContentBlock(
|
||||
)
|
||||
header = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(header)
|
||||
|
||||
content = "\n\n\n" + header + "\n\n" + content + "\n\n"
|
||||
content = "\n\n\n" + header + "\n\n" + content + "\n\n\n"
|
||||
}
|
||||
|
||||
return content
|
||||
@@ -229,7 +227,9 @@ func renderText(
|
||||
) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
||||
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).
|
||||
Local().
|
||||
Format("02 Jan 2006 03:04 PM")
|
||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
||||
// don't show the date if it's today
|
||||
timestamp = timestamp[12:]
|
||||
@@ -338,8 +338,10 @@ func renderToolDetails(
|
||||
finished := result != nil && *result != ""
|
||||
t := theme.CurrentTheme()
|
||||
backgroundColor := t.BackgroundPanel()
|
||||
borderColor := t.BackgroundPanel()
|
||||
if highlight {
|
||||
backgroundColor = t.BackgroundElement()
|
||||
borderColor = t.BorderActive()
|
||||
}
|
||||
|
||||
switch toolCall.ToolInvocation.ToolName {
|
||||
@@ -362,7 +364,11 @@ func renderToolDetails(
|
||||
diff.WithWidth(width-2),
|
||||
)
|
||||
body = strings.TrimSpace(formattedDiff)
|
||||
style := styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Padding(1, 2).Width(width - 4)
|
||||
style := styles.NewStyle().
|
||||
Background(backgroundColor).
|
||||
Foreground(t.TextMuted()).
|
||||
Padding(1, 2).
|
||||
Width(width - 4)
|
||||
if highlight {
|
||||
style = style.Foreground(t.Text()).Bold(true)
|
||||
}
|
||||
@@ -375,7 +381,14 @@ func renderToolDetails(
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
title = style.Render(title)
|
||||
content := title + "\n" + body
|
||||
content = renderContentBlock(app, content, highlight, width, WithPadding(0))
|
||||
content = renderContentBlock(
|
||||
app,
|
||||
content,
|
||||
highlight,
|
||||
width,
|
||||
WithPadding(0),
|
||||
WithBorderColor(borderColor),
|
||||
)
|
||||
return content
|
||||
}
|
||||
}
|
||||
@@ -478,7 +491,7 @@ func renderToolDetails(
|
||||
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
content := title + "\n\n" + body
|
||||
return renderContentBlock(app, content, highlight, width)
|
||||
return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
|
||||
}
|
||||
|
||||
func renderToolName(name string) string {
|
||||
@@ -489,8 +502,8 @@ func renderToolName(name string) string {
|
||||
return "Plan"
|
||||
default:
|
||||
normalizedName := name
|
||||
if strings.HasPrefix(name, "opencode_") {
|
||||
normalizedName = strings.TrimPrefix(name, "opencode_")
|
||||
if after, ok := strings.CutPrefix(name, "opencode_"); ok {
|
||||
normalizedName = after
|
||||
}
|
||||
return cases.Title(language.Und).String(normalizedName)
|
||||
}
|
||||
@@ -651,7 +664,10 @@ func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) s
|
||||
}
|
||||
line := diag.Range.Start.Line + 1 // 1-based
|
||||
column := diag.Range.Start.Character + 1 // 1-based
|
||||
errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
|
||||
errorDiagnostics = append(
|
||||
errorDiagnostics,
|
||||
fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
|
||||
)
|
||||
}
|
||||
if len(errorDiagnostics) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -309,9 +309,12 @@ func (m *messagesComponent) header(width int) string {
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
headerLines := []string{}
|
||||
headerLines = append(headerLines, util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||
headerLines = append(
|
||||
headerLines,
|
||||
util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
|
||||
)
|
||||
if m.app.Session.Share.URL != "" {
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL))
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL+" /unshare"))
|
||||
} else {
|
||||
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
@@ -575,7 +576,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
ansiSequences[visibleIdx] = lastAnsiSeq
|
||||
}
|
||||
visibleIdx++
|
||||
i++
|
||||
|
||||
// Properly advance by UTF-8 rune, not byte
|
||||
_, size := utf8.DecodeRuneInString(content[i:])
|
||||
i += size
|
||||
}
|
||||
|
||||
// Apply highlighting
|
||||
@@ -622,8 +626,9 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
}
|
||||
|
||||
// Get current character
|
||||
char := string(content[i])
|
||||
// Get current character (properly handle UTF-8)
|
||||
r, size := utf8.DecodeRuneInString(content[i:])
|
||||
char := string(r)
|
||||
|
||||
if inSelection {
|
||||
// Get the current styling
|
||||
@@ -657,7 +662,7 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
|
||||
currentPos++
|
||||
i++
|
||||
i += size
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
|
||||
@@ -37,7 +37,11 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m statusComponent) logo() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
|
||||
emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
|
||||
emphasis := styles.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.BackgroundElement()).
|
||||
Bold(true).
|
||||
Render
|
||||
|
||||
open := base("open")
|
||||
code := emphasis("code ")
|
||||
@@ -72,19 +76,16 @@ func formatTokensAndCost(tokens float64, contextWindow float64, cost float64) st
|
||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||
|
||||
return fmt.Sprintf("Context: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
||||
return fmt.Sprintf(
|
||||
"Context: %s (%d%%), Cost: %s",
|
||||
formattedTokens,
|
||||
int(percentage),
|
||||
formattedCost,
|
||||
)
|
||||
}
|
||||
|
||||
func (m statusComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
if m.app.Session.ID == "" {
|
||||
return styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(m.width).
|
||||
Height(2).
|
||||
Render("")
|
||||
}
|
||||
|
||||
logo := m.logo()
|
||||
|
||||
cwd := styles.NewStyle().
|
||||
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
)
|
||||
|
||||
const interruptDebounceTimeout = 1 * time.Second
|
||||
const fileViewerFullWidthCutoff = 200
|
||||
const fileViewerFullWidthCutoff = 160
|
||||
|
||||
type appModel struct {
|
||||
width, height int
|
||||
@@ -111,18 +111,19 @@ func isScrollRelatedInput(keyString string) bool {
|
||||
if len(keyString) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
for _, char := range keyString {
|
||||
charStr := string(char)
|
||||
if !BUGGED_SCROLL_KEYS[charStr] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(keyString) > 3 && (keyString[len(keyString)-1] == 'M' || keyString[len(keyString)-1] == 'm') {
|
||||
|
||||
if len(keyString) > 3 &&
|
||||
(keyString[len(keyString)-1] == 'M' || keyString[len(keyString)-1] == 'm') {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return len(keyString) > 1
|
||||
}
|
||||
|
||||
@@ -826,6 +827,17 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
shareUrl := response.Share.URL
|
||||
cmds = append(cmds, tea.SetClipboard(shareUrl))
|
||||
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
||||
case commands.SessionUnshareCommand:
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
_, err := a.app.Client.Session.Unshare(context.Background(), a.app.Session.ID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unshare session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to unshare session")
|
||||
}
|
||||
a.app.Session.Share.URL = ""
|
||||
cmds = append(cmds, toast.NewSuccessToast("Session unshared successfully"))
|
||||
case commands.SessionInterruptCommand:
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
|
||||
@@ -2,49 +2,39 @@ package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MapReducePar performs a parallel map-reduce operation on a slice of items.
|
||||
// It applies a function to each item in the slice concurrently,
|
||||
// and combines the results serially using a reducer returned from
|
||||
// each one of the functions, allowing the use of closures.
|
||||
func MapReducePar[a, b any](items []a, init b, fn func(a) func(b) b) b {
|
||||
itemCount := len(items)
|
||||
locks := make([]*sync.Mutex, itemCount)
|
||||
mapped := make([]func(b) b, itemCount)
|
||||
func mapParallel[in, out any](items []in, fn func(in) out) chan out {
|
||||
mapChans := make([]chan out, 0, len(items))
|
||||
|
||||
for i, value := range items {
|
||||
lock := &sync.Mutex{}
|
||||
lock.Lock()
|
||||
locks[i] = lock
|
||||
for _, v := range items {
|
||||
ch := make(chan out)
|
||||
mapChans = append(mapChans, ch)
|
||||
go func() {
|
||||
defer lock.Unlock()
|
||||
mapped[i] = fn(value)
|
||||
defer close(ch)
|
||||
ch <- fn(v)
|
||||
}()
|
||||
}
|
||||
|
||||
result := init
|
||||
for i := range itemCount {
|
||||
locks[i].Lock()
|
||||
defer locks[i].Unlock()
|
||||
f := mapped[i]
|
||||
if f != nil {
|
||||
result = f(result)
|
||||
}
|
||||
}
|
||||
resultChan := make(chan out)
|
||||
|
||||
return result
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
for _, ch := range mapChans {
|
||||
v := <-ch
|
||||
resultChan <- v
|
||||
}
|
||||
}()
|
||||
|
||||
return resultChan
|
||||
}
|
||||
|
||||
// WriteStringsPar allows to iterate over a list and compute strings in parallel,
|
||||
// yet write them in order.
|
||||
func WriteStringsPar[a any](sb *strings.Builder, items []a, fn func(a) string) {
|
||||
MapReducePar(items, sb, func(item a) func(*strings.Builder) *strings.Builder {
|
||||
str := fn(item)
|
||||
return func(sbdr *strings.Builder) *strings.Builder {
|
||||
sbdr.WriteString(str)
|
||||
return sbdr
|
||||
}
|
||||
})
|
||||
ch := mapParallel(items, fn)
|
||||
|
||||
for v := range ch {
|
||||
sb.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
func TestWriteStringsPar(t *testing.T) {
|
||||
items := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
sb := strings.Builder{}
|
||||
util.WriteStringsPar(&sb, items, func(i int) string {
|
||||
// sleep for the inverse duration so that later items finish first
|
||||
time.Sleep(time.Duration(10-i) * time.Millisecond)
|
||||
return strconv.Itoa(i)
|
||||
})
|
||||
if sb.String() != "0123456789" {
|
||||
t.Fatalf("expected 0123456789, got %s", sb.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user