mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-08 09:40:38 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774dcb6980 | ||
|
|
28bc49ad17 | ||
|
|
dc1947838c | ||
|
|
3ea2daaa4c | ||
|
|
137e964131 | ||
|
|
8efbe497fd | ||
|
|
119d2d966c | ||
|
|
194415e785 | ||
|
|
1684042fb6 |
@@ -1,24 +1,25 @@
|
||||
import type { CoreMessage } from "ai"
|
||||
import type { LanguageModelV1Prompt } from "ai"
|
||||
import { unique } from "remeda"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
export function message(
|
||||
msg: CoreMessage,
|
||||
index: number,
|
||||
msgs: LanguageModelV1Prompt,
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
) {
|
||||
if (
|
||||
(providerID === "anthropic" || modelID.includes("anthropic")) &&
|
||||
index < 4
|
||||
) {
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerMetadata = {
|
||||
...msg.providerMetadata,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg
|
||||
return msgs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type CoreMessage,
|
||||
type UIMessage,
|
||||
type ProviderMetadata,
|
||||
wrapLanguageModel,
|
||||
} from "ai"
|
||||
import { z, ZodSchema } from "zod"
|
||||
import { Decimal } from "decimal.js"
|
||||
@@ -247,7 +248,7 @@ export namespace Session {
|
||||
if (
|
||||
model.info.limit.context &&
|
||||
tokens >
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||
) {
|
||||
await summarize({
|
||||
sessionID: input.sessionID,
|
||||
@@ -285,9 +286,7 @@ export namespace Session {
|
||||
parts: toParts(input.parts),
|
||||
},
|
||||
]),
|
||||
].map((msg, i) =>
|
||||
ProviderTransform.message(msg, i, input.providerID, input.modelID),
|
||||
),
|
||||
],
|
||||
model: model.language,
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -296,7 +295,7 @@ export namespace Session {
|
||||
draft.title = result.text
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
}
|
||||
const msg: Message.Info = {
|
||||
role: "user",
|
||||
@@ -527,12 +526,26 @@ export namespace Session {
|
||||
...convertToCoreMessages(
|
||||
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||
),
|
||||
].map((msg, i) =>
|
||||
ProviderTransform.message(msg, i, input.providerID, input.modelID),
|
||||
),
|
||||
],
|
||||
temperature: model.info.temperature ? 0 : undefined,
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: model.language,
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
args.params.prompt = ProviderTransform.message(
|
||||
args.params.prompt,
|
||||
input.providerID,
|
||||
input.modelID,
|
||||
)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
try {
|
||||
for await (const value of result.fullStream) {
|
||||
@@ -559,7 +572,7 @@ export namespace Session {
|
||||
case "tool-call": {
|
||||
const [match] = next.parts.flatMap((p) =>
|
||||
p.type === "tool-invocation" &&
|
||||
p.toolInvocation.toolCallId === value.toolCallId
|
||||
p.toolInvocation.toolCallId === value.toolCallId
|
||||
? [p]
|
||||
: [],
|
||||
)
|
||||
@@ -723,7 +736,9 @@ export namespace Session {
|
||||
},
|
||||
}
|
||||
await updateMessage(next)
|
||||
const result = await generateText({
|
||||
|
||||
let text: Message.TextPart | undefined
|
||||
const result = streamText({
|
||||
abortSignal: abort.signal,
|
||||
model: model.language,
|
||||
messages: [
|
||||
@@ -744,16 +759,46 @@ export namespace Session {
|
||||
],
|
||||
},
|
||||
],
|
||||
onStepFinish: async (step) => {
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||
assistant.cost += usage.cost
|
||||
assistant.tokens = usage.tokens
|
||||
await updateMessage(next)
|
||||
if (text) {
|
||||
Bus.publish(Message.Event.PartUpdated, {
|
||||
part: text,
|
||||
messageID: next.id,
|
||||
sessionID: next.metadata.sessionID,
|
||||
})
|
||||
}
|
||||
text = undefined
|
||||
},
|
||||
async onFinish(input) {
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||
assistant.cost = usage.cost
|
||||
assistant.tokens = usage.tokens
|
||||
next.metadata!.time.completed = Date.now()
|
||||
await updateMessage(next)
|
||||
},
|
||||
})
|
||||
next.parts.push({
|
||||
type: "text",
|
||||
text: result.text,
|
||||
})
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(model.info, result.usage, result.providerMetadata)
|
||||
assistant.cost = usage.cost
|
||||
assistant.tokens = usage.tokens
|
||||
await updateMessage(next)
|
||||
|
||||
for await (const value of result.fullStream) {
|
||||
switch (value.type) {
|
||||
case "text-delta":
|
||||
if (!text) {
|
||||
text = {
|
||||
type: "text",
|
||||
text: value.textDelta,
|
||||
}
|
||||
next.parts.push(text)
|
||||
} else text.text += value.textDelta
|
||||
|
||||
await updateMessage(next)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lock(sessionID: string) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
You will generate a short title based on the first message a user begins a conversation with
|
||||
- ensure it is not more than 50 characters long
|
||||
- the title should be a summary of the user's message
|
||||
- it should be one line long
|
||||
- do not use quotes or colons
|
||||
- the entire text you return will be used as the title
|
||||
- never return anything that is more than one sentence (one line) long
|
||||
Generate a short title based on the first message a user begins a conversation with. CRITICAL: Your response must be EXACTLY one line with NO line breaks, newlines, or multiple sentences.
|
||||
|
||||
Requirements:
|
||||
- Maximum 50 characters
|
||||
- Single line only - NO newlines or line breaks
|
||||
- Summary of the user's message
|
||||
- No quotes, colons, or special formatting
|
||||
- Do not include explanatory text like "summary:" or similar
|
||||
- Your entire response becomes the title
|
||||
|
||||
IMPORTANT: Return only the title text on a single line. Do not add any explanations, formatting, or additional text.
|
||||
|
||||
@@ -26,7 +26,11 @@ func main() {
|
||||
|
||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||
var appInfo client.AppInfo
|
||||
json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal app info", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
|
||||
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
||||
|
||||
@@ -126,6 +126,10 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
// TODO: notify user
|
||||
return nil
|
||||
}
|
||||
if providersResponse != nil && providersResponse.StatusCode() != 200 {
|
||||
slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
|
||||
return nil
|
||||
}
|
||||
providers := []client.ProviderInfo{}
|
||||
var defaultProvider *client.ProviderInfo
|
||||
var defaultModel *client.ModelInfo
|
||||
@@ -248,17 +252,19 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||
}
|
||||
|
||||
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
}
|
||||
if response != nil && response.StatusCode() != 200 {
|
||||
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||
}
|
||||
go func() {
|
||||
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
}
|
||||
if response != nil && response.StatusCode() != 200 {
|
||||
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
||||
r := styles.GetMarkdownRenderer(width, backgroundColor)
|
||||
content = strings.ReplaceAll(content, app.RootPath+"/", "")
|
||||
content = strings.ReplaceAll(content, "<", "\\<")
|
||||
content = strings.ReplaceAll(content, ">", "\\>")
|
||||
rendered, _ := r.Render(content)
|
||||
lines := strings.Split(rendered, "\n")
|
||||
|
||||
@@ -44,7 +46,6 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content = strings.Join(lines, "\n")
|
||||
return strings.TrimSuffix(content, "\n")
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
commandsComponent "github.com/sst/opencode/internal/components/commands"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type helpDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
commands []commands.Command
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
app *app.App
|
||||
commandsComponent commandsComponent.CommandsComponent
|
||||
}
|
||||
|
||||
func (h *helpDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
return h.commandsComponent.Init()
|
||||
}
|
||||
|
||||
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -27,45 +26,17 @@ func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.WindowSizeMsg:
|
||||
h.width = msg.Width
|
||||
h.height = msg.Height
|
||||
h.commandsComponent.SetSize(msg.Width, msg.Height)
|
||||
}
|
||||
return h, nil
|
||||
|
||||
_, cmd := h.commandsComponent.Update(msg)
|
||||
return h, cmd
|
||||
}
|
||||
|
||||
func (h *helpDialog) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
keyStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text()).
|
||||
Bold(true)
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.TextMuted())
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
PaddingLeft(1).Background(t.BackgroundElement())
|
||||
|
||||
lines := []string{}
|
||||
for _, b := range h.commands {
|
||||
// Only interested in slash commands
|
||||
if b.Trigger == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
content := keyStyle.Render("/" + b.Trigger)
|
||||
content += descStyle.Render(" " + b.Description)
|
||||
// for i, key := range b.Keybindings {
|
||||
// if i == 0 {
|
||||
// keyString := " (" + key.Key + ")"
|
||||
// space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
|
||||
// spacer := strings.Repeat(" ", space)
|
||||
// content += descStyle.Render(spacer)
|
||||
// content += descStyle.Render(keyString)
|
||||
// }
|
||||
// }
|
||||
|
||||
lines = append(lines, contentStyle.Render(content))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
h.commandsComponent.SetBackgroundColor(t.BackgroundElement())
|
||||
return h.commandsComponent.View()
|
||||
}
|
||||
|
||||
func (h *helpDialog) Render(background string) string {
|
||||
@@ -80,9 +51,10 @@ type HelpDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
func NewHelpDialog(commands []commands.Command) HelpDialog {
|
||||
func NewHelpDialog(app *app.App) HelpDialog {
|
||||
return &helpDialog{
|
||||
commands: commands,
|
||||
modal: modal.New(modal.WithTitle("Help")),
|
||||
app: app,
|
||||
commandsComponent: commandsComponent.New(app, commandsComponent.WithBackground(theme.CurrentTheme().BackgroundElement())),
|
||||
modal: modal.New(modal.WithTitle("Help")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -33,20 +34,15 @@ type modelDialog struct {
|
||||
app *app.App
|
||||
availableProviders []client.ProviderInfo
|
||||
provider client.ProviderInfo
|
||||
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
scrollOffset int
|
||||
hScrollOffset int
|
||||
hScrollPossible bool
|
||||
|
||||
modal *modal.Modal
|
||||
width int
|
||||
height int
|
||||
hScrollOffset int
|
||||
hScrollPossible bool
|
||||
modal *modal.Modal
|
||||
modelList list.List[list.StringItem]
|
||||
}
|
||||
|
||||
type modelKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Enter key.Binding
|
||||
@@ -54,14 +50,6 @@ type modelKeyMap struct {
|
||||
}
|
||||
|
||||
var modelKeys = modelKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑", "previous model"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓", "next model"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←", "scroll left"),
|
||||
@@ -81,15 +69,7 @@ var modelKeys = modelKeyMap{
|
||||
}
|
||||
|
||||
func (m *modelDialog) Init() tea.Cmd {
|
||||
// cfg := config.Get()
|
||||
// modelInfo := GetSelectedModel(cfg)
|
||||
// m.availableProviders = getEnabledProviders(cfg)
|
||||
// m.hScrollPossible = len(m.availableProviders) > 1
|
||||
|
||||
// m.provider = modelInfo.Provider
|
||||
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
|
||||
|
||||
// m.setupModelsForProvider(m.provider)
|
||||
m.setupModelsForProvider(m.provider.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -97,26 +77,32 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, modelKeys.Up):
|
||||
m.moveSelectionUp()
|
||||
case key.Matches(msg, modelKeys.Down):
|
||||
m.moveSelectionDown()
|
||||
case key.Matches(msg, modelKeys.Left):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(-1)
|
||||
}
|
||||
return m, nil
|
||||
case key.Matches(msg, modelKeys.Right):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(1)
|
||||
}
|
||||
return m, nil
|
||||
case key.Matches(msg, modelKeys.Enter):
|
||||
selectedItem, _ := m.modelList.GetSelectedItem()
|
||||
models := m.models()
|
||||
var selectedModel client.ModelInfo
|
||||
for _, model := range models {
|
||||
if model.Name == string(selectedItem) {
|
||||
selectedModel = model
|
||||
break
|
||||
}
|
||||
}
|
||||
return m, tea.Sequence(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(
|
||||
app.ModelSelectedMsg{
|
||||
Provider: m.provider,
|
||||
Model: models[m.selectedIdx],
|
||||
Model: selectedModel,
|
||||
}),
|
||||
)
|
||||
case key.Matches(msg, modelKeys.Escape):
|
||||
@@ -127,7 +113,10 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.height = msg.Height
|
||||
}
|
||||
|
||||
return m, nil
|
||||
// Update the list component
|
||||
updatedList, cmd := m.modelList.Update(msg)
|
||||
m.modelList = updatedList.(list.List[list.StringItem])
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *modelDialog) models() []client.ModelInfo {
|
||||
@@ -137,40 +126,9 @@ func (m *modelDialog) models() []client.ModelInfo {
|
||||
return models
|
||||
}
|
||||
|
||||
// moveSelectionUp moves the selection up or wraps to bottom
|
||||
func (m *modelDialog) moveSelectionUp() {
|
||||
if m.selectedIdx > 0 {
|
||||
m.selectedIdx--
|
||||
} else {
|
||||
m.selectedIdx = len(m.provider.Models) - 1
|
||||
m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels)
|
||||
}
|
||||
|
||||
// Keep selection visible
|
||||
if m.selectedIdx < m.scrollOffset {
|
||||
m.scrollOffset = m.selectedIdx
|
||||
}
|
||||
}
|
||||
|
||||
// moveSelectionDown moves the selection down or wraps to top
|
||||
func (m *modelDialog) moveSelectionDown() {
|
||||
if m.selectedIdx < len(m.provider.Models)-1 {
|
||||
m.selectedIdx++
|
||||
} else {
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
}
|
||||
|
||||
// Keep selection visible
|
||||
if m.selectedIdx >= m.scrollOffset+numVisibleModels {
|
||||
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelDialog) switchProvider(offset int) {
|
||||
newOffset := m.hScrollOffset + offset
|
||||
|
||||
// Ensure we stay within bounds
|
||||
if newOffset < 0 {
|
||||
newOffset = len(m.availableProviders) - 1
|
||||
}
|
||||
@@ -185,105 +143,46 @@ func (m *modelDialog) switchProvider(offset int) {
|
||||
}
|
||||
|
||||
func (m *modelDialog) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text())
|
||||
|
||||
// Render visible models
|
||||
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
|
||||
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
||||
|
||||
models := m.models()
|
||||
for i := m.scrollOffset; i < endIdx; i++ {
|
||||
itemStyle := baseStyle.Width(maxDialogWidth)
|
||||
if i == m.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Bold(true)
|
||||
}
|
||||
modelItems = append(modelItems, itemStyle.Render(models[i].Name))
|
||||
}
|
||||
|
||||
listView := m.modelList.View()
|
||||
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
baseStyle.
|
||||
Width(maxDialogWidth).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
||||
scrollIndicator,
|
||||
)
|
||||
|
||||
return content
|
||||
return strings.Join([]string{listView, scrollIndicator}, "\n")
|
||||
}
|
||||
|
||||
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||
var indicator string
|
||||
|
||||
if len(m.provider.Models) > numVisibleModels {
|
||||
if m.scrollOffset > 0 {
|
||||
indicator += "↑ "
|
||||
}
|
||||
if m.scrollOffset+numVisibleModels < len(m.provider.Models) {
|
||||
indicator += "↓ "
|
||||
}
|
||||
}
|
||||
|
||||
if m.hScrollPossible {
|
||||
indicator = "← " + indicator + "→"
|
||||
indicator = "← → (switch provider) "
|
||||
}
|
||||
|
||||
if indicator == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
return baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
return styles.BaseStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Width(maxWidth).
|
||||
Align(lipgloss.Right).
|
||||
Bold(true).
|
||||
Render(indicator)
|
||||
}
|
||||
|
||||
// findProviderIndex returns the index of the provider in the list, or -1 if not found
|
||||
// func findProviderIndex(providers []string, provider string) int {
|
||||
// for i, p := range providers {
|
||||
// if p == provider {
|
||||
// return i
|
||||
// }
|
||||
// }
|
||||
// return -1
|
||||
// }
|
||||
func (m *modelDialog) setupModelsForProvider(providerId string) {
|
||||
models := m.models()
|
||||
modelNames := make([]string, len(models))
|
||||
for i, model := range models {
|
||||
modelNames[i] = model.Name
|
||||
}
|
||||
|
||||
func (m *modelDialog) setupModelsForProvider(_ string) {
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
|
||||
m.modelList.SetMaxWidth(maxDialogWidth)
|
||||
|
||||
// cfg := config.Get()
|
||||
// agentCfg := cfg.Agents[config.AgentPrimary]
|
||||
// selectedModelId := agentCfg.Model
|
||||
|
||||
// m.provider = provider
|
||||
// m.models = getModelsForProvider(provider)
|
||||
|
||||
// Try to select the current model if it belongs to this provider
|
||||
// if provider == models.SupportedModels[selectedModelId].Provider {
|
||||
// for i, model := range m.models {
|
||||
// if model.ID == selectedModelId {
|
||||
// m.selectedIdx = i
|
||||
// // Adjust scroll position to keep selected model visible
|
||||
// if m.selectedIdx >= numVisibleModels {
|
||||
// m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
|
||||
for i, model := range models {
|
||||
if model.Id == m.app.Model.Id {
|
||||
m.modelList.SetSelectedIndex(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelDialog) Render(background string) string {
|
||||
@@ -297,11 +196,30 @@ func (s *modelDialog) Close() tea.Cmd {
|
||||
func NewModelDialog(app *app.App) ModelDialog {
|
||||
availableProviders, _ := app.ListProviders(context.Background())
|
||||
|
||||
return &modelDialog{
|
||||
availableProviders: availableProviders,
|
||||
hScrollOffset: 0,
|
||||
hScrollPossible: len(availableProviders) > 1,
|
||||
provider: availableProviders[0],
|
||||
modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
|
||||
currentProvider := availableProviders[0]
|
||||
hScrollOffset := 0
|
||||
if app.Provider != nil {
|
||||
for i, provider := range availableProviders {
|
||||
if provider.Id == app.Provider.Id {
|
||||
currentProvider = provider
|
||||
hScrollOffset = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog := &modelDialog{
|
||||
app: app,
|
||||
availableProviders: availableProviders,
|
||||
hScrollOffset: hScrollOffset,
|
||||
hScrollPossible: len(availableProviders) > 1,
|
||||
provider: currentProvider,
|
||||
modal: modal.New(
|
||||
modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
|
||||
modal.WithMaxWidth(maxDialogWidth+4),
|
||||
),
|
||||
}
|
||||
|
||||
dialog.setupModelsForProvider(currentProvider.Id)
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
@@ -19,33 +17,12 @@ type SessionDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type sessionItem client.SessionInfo
|
||||
|
||||
func (s sessionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().
|
||||
Width(width - 4).
|
||||
Background(t.BackgroundElement())
|
||||
|
||||
if selected {
|
||||
baseStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Bold(true)
|
||||
} else {
|
||||
baseStyle = baseStyle.
|
||||
Foreground(t.Text())
|
||||
}
|
||||
|
||||
return baseStyle.Padding(0, 1).Render(s.Title)
|
||||
}
|
||||
|
||||
type sessionDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
selectedSessionID string
|
||||
list list.List[sessionItem]
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
sessions []client.SessionInfo
|
||||
list list.List[list.StringItem]
|
||||
}
|
||||
|
||||
func (s *sessionDialog) Init() tea.Cmd {
|
||||
@@ -61,11 +38,11 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
||||
s.selectedSessionID = item.Id
|
||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||
selectedSession := s.sessions[idx]
|
||||
return s, tea.Sequence(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(app.SessionSelectedMsg(&item)),
|
||||
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +50,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := s.list.Update(msg)
|
||||
s.list = listModel.(list.List[sessionItem])
|
||||
s.list = listModel.(list.List[list.StringItem])
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
@@ -89,23 +66,30 @@ func (s *sessionDialog) Close() tea.Cmd {
|
||||
func NewSessionDialog(app *app.App) SessionDialog {
|
||||
sessions, _ := app.ListSessions(context.Background())
|
||||
|
||||
var sessionItems []sessionItem
|
||||
var filteredSessions []client.SessionInfo
|
||||
var sessionTitles []string
|
||||
for _, sess := range sessions {
|
||||
if sess.ParentID != nil {
|
||||
continue
|
||||
}
|
||||
sessionItems = append(sessionItems, sessionItem(sess))
|
||||
filteredSessions = append(filteredSessions, sess)
|
||||
sessionTitles = append(sessionTitles, sess.Title)
|
||||
}
|
||||
|
||||
list := list.NewListComponent(
|
||||
sessionItems,
|
||||
list := list.NewStringList(
|
||||
sessionTitles,
|
||||
10, // maxVisibleSessions
|
||||
"No sessions available",
|
||||
true, // useAlphaNumericKeys
|
||||
)
|
||||
list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||
|
||||
return &sessionDialog{
|
||||
list: list,
|
||||
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
|
||||
sessions: filteredSessions,
|
||||
list: list,
|
||||
modal: modal.New(
|
||||
modal.WithTitle("Switch Session"),
|
||||
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
list "github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
@@ -20,35 +19,12 @@ type ThemeDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type themeItem struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (t themeItem) Render(selected bool, width int) string {
|
||||
th := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().
|
||||
Width(width - 2).
|
||||
Background(th.BackgroundElement())
|
||||
|
||||
if selected {
|
||||
baseStyle = baseStyle.
|
||||
Background(th.Primary()).
|
||||
Foreground(th.BackgroundElement()).
|
||||
Bold(true)
|
||||
} else {
|
||||
baseStyle = baseStyle.
|
||||
Foreground(th.Text())
|
||||
}
|
||||
|
||||
return baseStyle.Padding(0, 1).Render(t.name)
|
||||
}
|
||||
|
||||
type themeDialog struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
modal *modal.Modal
|
||||
list list.List[themeItem]
|
||||
list list.List[list.StringItem]
|
||||
originalTheme string
|
||||
themeApplied bool
|
||||
}
|
||||
@@ -66,7 +42,7 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
|
||||
selectedTheme := item.name
|
||||
selectedTheme := string(item)
|
||||
if err := theme.SetTheme(selectedTheme); err != nil {
|
||||
// status.Error(err.Error())
|
||||
return t, nil
|
||||
@@ -85,11 +61,11 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := t.list.Update(msg)
|
||||
t.list = listModel.(list.List[themeItem])
|
||||
t.list = listModel.(list.List[list.StringItem])
|
||||
|
||||
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
|
||||
theme.SetTheme(item.name)
|
||||
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: item.name})
|
||||
theme.SetTheme(string(item))
|
||||
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(item)})
|
||||
}
|
||||
return t, cmd
|
||||
}
|
||||
@@ -101,6 +77,7 @@ func (t *themeDialog) Render(background string) string {
|
||||
func (t *themeDialog) Close() tea.Cmd {
|
||||
if !t.themeApplied {
|
||||
theme.SetTheme(t.originalTheme)
|
||||
return util.CmdHandler(ThemeSelectedMsg{ThemeName: t.originalTheme})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -110,17 +87,15 @@ func NewThemeDialog() ThemeDialog {
|
||||
themes := theme.AvailableThemes()
|
||||
currentTheme := theme.CurrentThemeName()
|
||||
|
||||
var themeItems []themeItem
|
||||
var selectedIdx int
|
||||
for i, name := range themes {
|
||||
themeItems = append(themeItems, themeItem{name: name})
|
||||
if name == currentTheme {
|
||||
selectedIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
list := list.NewListComponent(
|
||||
themeItems,
|
||||
list := list.NewStringList(
|
||||
themes,
|
||||
10, // maxVisibleThemes
|
||||
"No themes available",
|
||||
true,
|
||||
@@ -128,6 +103,9 @@ func NewThemeDialog() ThemeDialog {
|
||||
|
||||
// Set the initial selection to the current theme
|
||||
list.SetSelectedIndex(selectedIdx)
|
||||
|
||||
// Set the max width for the list to match the modal width
|
||||
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
|
||||
|
||||
return &themeDialog{
|
||||
list: list,
|
||||
|
||||
@@ -5,6 +5,10 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type ListItem interface {
|
||||
@@ -123,6 +127,9 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
|
||||
func (c *listComponent[T]) View() string {
|
||||
items := c.items
|
||||
maxWidth := c.maxWidth
|
||||
if maxWidth == 0 {
|
||||
maxWidth = 80 // Default width if not set
|
||||
}
|
||||
maxVisibleItems := min(c.maxVisibleItems, len(items))
|
||||
startIdx := 0
|
||||
|
||||
@@ -161,3 +168,36 @@ func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg st
|
||||
selectedIdx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// StringItem is a simple implementation of ListItem for string values
|
||||
type StringItem string
|
||||
|
||||
func (s StringItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
|
||||
|
||||
var itemStyle lipgloss.Style
|
||||
if selected {
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
} else {
|
||||
itemStyle = baseStyle.
|
||||
PaddingLeft(1)
|
||||
}
|
||||
|
||||
return itemStyle.Render(truncatedStr)
|
||||
}
|
||||
|
||||
// NewStringList creates a new list component with string items
|
||||
func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] {
|
||||
stringItems := make([]StringItem, len(items))
|
||||
for i, item := range items {
|
||||
stringItems[i] = StringItem(item)
|
||||
}
|
||||
return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
|
||||
}
|
||||
|
||||
@@ -103,13 +103,13 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
|
||||
escStyle := baseStyle.Foreground(t.TextMuted())
|
||||
escText := escStyle.Render("esc")
|
||||
|
||||
// Calculate position for esc text
|
||||
titleWidth := lipgloss.Width(m.title)
|
||||
escWidth := lipgloss.Width(escText)
|
||||
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
|
||||
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
|
||||
spacer := strings.Repeat(" ", spacesNeeded)
|
||||
titleLine := m.title + spacer + escText
|
||||
titleLine = titleStyle.Render(titleLine)
|
||||
|
||||
@@ -71,7 +71,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||
|
||||
return fmt.Sprintf("Tokens: %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 {
|
||||
|
||||
77
packages/tui/internal/theme/themes/matrix.json
Normal file
77
packages/tui/internal/theme/themes/matrix.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"matrixInk0": "#0a0e0a",
|
||||
"matrixInk1": "#0e130d",
|
||||
"matrixInk2": "#141c12",
|
||||
"matrixInk3": "#1e2a1b",
|
||||
"rainGreen": "#2eff6a",
|
||||
"rainGreenDim": "#1cc24b",
|
||||
"rainGreenHi": "#62ff94",
|
||||
"rainCyan": "#00efff",
|
||||
"rainTeal": "#24f6d9",
|
||||
"rainPurple": "#c770ff",
|
||||
"rainOrange": "#ffa83d",
|
||||
"alertRed": "#ff4b4b",
|
||||
"alertYellow": "#e6ff57",
|
||||
"alertBlue": "#30b3ff",
|
||||
"rainGray": "#8ca391",
|
||||
"lightBg": "#eef3ea",
|
||||
"lightPaper": "#e4ebe1",
|
||||
"lightInk1": "#dae1d7",
|
||||
"lightText": "#203022",
|
||||
"lightGray": "#748476"
|
||||
},
|
||||
"theme": {
|
||||
"primary": { "dark": "rainGreen", "light": "rainGreenDim" },
|
||||
"secondary": { "dark": "rainCyan", "light": "rainTeal" },
|
||||
"accent": { "dark": "rainPurple", "light": "rainPurple" },
|
||||
"error": { "dark": "alertRed", "light": "alertRed" },
|
||||
"warning": { "dark": "alertYellow", "light": "alertYellow" },
|
||||
"success": { "dark": "rainGreenHi", "light": "rainGreenDim" },
|
||||
"info": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"text": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"textMuted": { "dark": "rainGray", "light": "lightGray" },
|
||||
"background": { "dark": "matrixInk0", "light": "lightBg" },
|
||||
"backgroundPanel": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||
"backgroundElement": { "dark": "matrixInk2", "light": "lightInk1" },
|
||||
"border": { "dark": "matrixInk3", "light": "lightGray" },
|
||||
"borderActive": { "dark": "rainGreen", "light": "rainGreenDim" },
|
||||
"borderSubtle": { "dark": "matrixInk2", "light": "lightInk1" },
|
||||
"diffAdded": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||
"diffRemoved": { "dark": "alertRed", "light": "alertRed" },
|
||||
"diffContext": { "dark": "rainGray", "light": "lightGray" },
|
||||
"diffHunkHeader": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"diffHighlightAdded": { "dark": "#77ffaf", "light": "#5dac7e" },
|
||||
"diffHighlightRemoved": { "dark": "#ff7171", "light": "#d53a3a" },
|
||||
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
|
||||
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
|
||||
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||
"diffLineNumber": { "dark": "matrixInk3", "light": "lightGray" },
|
||||
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
|
||||
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
|
||||
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"markdownHeading": { "dark": "rainCyan", "light": "rainTeal" },
|
||||
"markdownLink": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"markdownLinkText": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"markdownCode": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||
"markdownBlockQuote": { "dark": "rainGray", "light": "lightGray" },
|
||||
"markdownEmph": { "dark": "rainOrange", "light": "rainOrange" },
|
||||
"markdownStrong": { "dark": "alertYellow", "light": "alertYellow" },
|
||||
"markdownHorizontalRule": { "dark": "rainGray", "light": "lightGray" },
|
||||
"markdownListItem": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"markdownListEnumeration": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"markdownImage": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"markdownImageText": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"markdownCodeBlock": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"syntaxComment": { "dark": "rainGray", "light": "lightGray" },
|
||||
"syntaxKeyword": { "dark": "rainPurple", "light": "rainPurple" },
|
||||
"syntaxFunction": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"syntaxVariable": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"syntaxString": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||
"syntaxNumber": { "dark": "rainOrange", "light": "rainOrange" },
|
||||
"syntaxType": { "dark": "alertYellow", "light": "alertYellow" },
|
||||
"syntaxOperator": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"syntaxPunctuation": { "dark": "rainGreenHi", "light": "lightText" }
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
}
|
||||
switch command.Name {
|
||||
case commands.AppHelpCommand:
|
||||
helpDialog := dialog.NewHelpDialog(a.app.Commands.Sorted())
|
||||
helpDialog := dialog.NewHelpDialog(a.app)
|
||||
a.modal = helpDialog
|
||||
case commands.EditorOpenCommand:
|
||||
if a.app.IsBusy() {
|
||||
|
||||
Reference in New Issue
Block a user