mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 05:19:33 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5746424272 |
No files matched your search
@@ -142,13 +142,6 @@ the key in the wizard:
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export ANTHROPIC_API_KEY=...
|
||||
export GEMINI_API_KEY=...
|
||||
export LONGCAT_API_KEY=...
|
||||
```
|
||||
|
||||
To configure Meituan LongCat (LongCat-2.0) directly, run:
|
||||
|
||||
```bash
|
||||
zero providers setup longcat --set-active
|
||||
```
|
||||
|
||||
For local models, run Ollama or LM Studio and then use `zero setup` or
|
||||
|
||||
@@ -18,7 +18,6 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
}{
|
||||
{id: "openai", name: "OpenAI", transport: "openai", defaultModel: "gpt-4.1"},
|
||||
{id: "groq", name: "Groq", transport: "openai-compatible", defaultModel: "llama-3.3-70b-versatile"},
|
||||
{id: "longcat", name: "LongCat", transport: "openai-compatible", defaultModel: "LongCat-2.0"},
|
||||
{id: "ollama-cloud", name: "Ollama Cloud", transport: "openai-compatible", defaultModel: "qwen3-coder:480b"},
|
||||
{id: "ollama", name: "Ollama Local", transport: "openai-compatible", defaultModel: "llama3.1"},
|
||||
}
|
||||
@@ -47,21 +46,6 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCatalogLongCatRequiresAPIKey(t *testing.T) {
|
||||
output := runProviderCatalogOnboarding(t)
|
||||
block := providerCatalogOnboardingBlock(t, output, "longcat")
|
||||
|
||||
for _, want := range []string{
|
||||
"requiresAuth=true",
|
||||
"authEnvVars=LONGCAT_API_KEY",
|
||||
"setup: zero providers setup longcat --set-active",
|
||||
} {
|
||||
if !strings.Contains(block, want) {
|
||||
t.Fatalf("expected LongCat catalog block to contain %q, got:\n%s", want, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCatalogOllamaCloudRequiresAPIKey(t *testing.T) {
|
||||
output := runProviderCatalogOnboarding(t)
|
||||
block := providerCatalogOnboardingBlock(t, output, "ollama-cloud")
|
||||
|
||||
@@ -123,7 +123,6 @@ var descriptors = []Descriptor{
|
||||
openAICompat("together", "Together AI", "https://api.together.xyz/v1", "meta-llama/Llama-3.3-70B-Instruct-Turbo", []string{"TOGETHER_API_KEY"}),
|
||||
openAICompat("dashscope", "DashScope", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-plus", []string{"DASHSCOPE_API_KEY", "QWEN_API_KEY"}, "qwen"),
|
||||
openAICompat("moonshot", "Moonshot AI", "https://api.moonshot.ai/v1", "kimi-k2-0905-preview", []string{"MOONSHOT_API_KEY"}, "kimi"),
|
||||
openAICompat("longcat", "LongCat", "https://api.longcat.chat/openai", "LongCat-2.0", []string{"LONGCAT_API_KEY"}, "meituan longcat", "meituan", "longcat-2.0"),
|
||||
openAICompat("nvidia-nim", "NVIDIA NIM", "https://integrate.api.nvidia.com/v1", "nvidia/llama-3.1-nemotron-70b-instruct", []string{"NVIDIA_API_KEY"}, "nvidia nim"),
|
||||
anthropicCompat("minimax", "MiniMax", "https://api.minimax.io/anthropic", "MiniMax-M3", []string{"MINIMAX_API_KEY"}, "mini-max", "mini_max"),
|
||||
openAICompat("mistral", "Mistral", "https://api.mistral.ai/v1", "mistral-large-latest", []string{"MISTRAL_API_KEY"}),
|
||||
|
||||
@@ -23,7 +23,6 @@ var expectedCatalogIDs = []string{
|
||||
"together",
|
||||
"dashscope",
|
||||
"moonshot",
|
||||
"longcat",
|
||||
"nvidia-nim",
|
||||
"minimax",
|
||||
"mistral",
|
||||
@@ -100,28 +99,6 @@ func TestRecommendedProviderEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongCatDescriptor(t *testing.T) {
|
||||
descriptor, err := Require("longcat")
|
||||
if err != nil {
|
||||
t.Fatalf("Require(longcat) error = %v", err)
|
||||
}
|
||||
if descriptor.Name != "LongCat" {
|
||||
t.Fatalf("Name = %q, want LongCat", descriptor.Name)
|
||||
}
|
||||
if descriptor.DefaultBaseURL != "https://api.longcat.chat/openai" {
|
||||
t.Fatalf("DefaultBaseURL = %q, want LongCat OpenAI-compatible endpoint", descriptor.DefaultBaseURL)
|
||||
}
|
||||
if descriptor.DefaultModel != "LongCat-2.0" {
|
||||
t.Fatalf("DefaultModel = %q, want LongCat-2.0", descriptor.DefaultModel)
|
||||
}
|
||||
if descriptor.Transport != TransportOpenAICompatible {
|
||||
t.Fatalf("Transport = %q, want %q", descriptor.Transport, TransportOpenAICompatible)
|
||||
}
|
||||
if !reflect.DeepEqual(descriptor.AuthEnvVars, []string{"LONGCAT_API_KEY"}) {
|
||||
t.Fatalf("AuthEnvVars = %#v, want LONGCAT_API_KEY", descriptor.AuthEnvVars)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogDescriptorsExposeRequiredDefaults(t *testing.T) {
|
||||
for _, descriptor := range All() {
|
||||
if descriptor.ID == "" {
|
||||
@@ -290,7 +267,7 @@ func TestListByTransportPreservesCatalogOrder(t *testing.T) {
|
||||
TransportBedrock: {"bedrock"},
|
||||
TransportVertex: {"vertex"},
|
||||
TransportAnthropicCompat: {"minimax", "custom-anthropic-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "ollama-cloud", "ollama", "lmstudio", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "dashscope", "moonshot", "longcat", "nvidia-nim", "mistral", "github", "xai", "venice", "xiaomi-mimo", "bankr", "zai", "kilocode", "opencode", "opencode-go", "atomic-chat", "chatgpt-proxy", "custom-openai-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "ollama-cloud", "ollama", "lmstudio", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "dashscope", "moonshot", "nvidia-nim", "mistral", "github", "xai", "venice", "xiaomi-mimo", "bankr", "zai", "kilocode", "opencode", "opencode-go", "atomic-chat", "chatgpt-proxy", "custom-openai-compatible"},
|
||||
}
|
||||
|
||||
for transport, wantIDs := range cases {
|
||||
|
||||
@@ -133,6 +133,18 @@ func (m model) renderKeybindingHelpOverlay(width int, height int) string {
|
||||
return centerRenderedBlock(block, width)
|
||||
}
|
||||
|
||||
// keybindingHelpOverlay returns the `?` shortcut overlay as a centered block to
|
||||
// COMPOSITE over the transcript — the same viewport-overlay path the model
|
||||
// picker and suggestions use — or "" when it is not open. Rendering it this way
|
||||
// (rather than replacing the whole View) keeps the chat visible behind it, so
|
||||
// `?` behaves like every other popup instead of blanking the screen (#419).
|
||||
func (m model) keybindingHelpOverlay(width int) string {
|
||||
if !m.helpOverlay {
|
||||
return ""
|
||||
}
|
||||
return m.renderKeybindingHelpOverlay(width, m.height)
|
||||
}
|
||||
|
||||
// keybindingHelpOverlayWidth picks the overlay width: wide enough that the
|
||||
// descriptions don't truncate next to the key column, capped, and never wider
|
||||
// than the terminal.
|
||||
|
||||
@@ -105,3 +105,56 @@ func TestKeybindingGroupsAreWellFormed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #419: the `?` help overlay must render ON TOP of the chat (like the model
|
||||
// picker), not REPLACE the whole screen. The old full-screen replace produced
|
||||
// only the centered shortcut block on a blank canvas — no title bar, no
|
||||
// composer. So with the overlay open, the surrounding chat chrome that is NOT
|
||||
// covered by the centered box (the model title bar at top, the composer at
|
||||
// bottom) must still be present alongside "Keyboard Shortcuts".
|
||||
func TestHelpOverlayCompositesOverChatNotReplacingIt(t *testing.T) {
|
||||
m := newModel(context.Background(), Options{ModelName: "gpt-4o"})
|
||||
m.width = 100
|
||||
m.height = 40
|
||||
m.altScreen = true
|
||||
|
||||
base := plainRender(t, m.View()) // no overlay: baseline chrome
|
||||
m.helpOverlay = true
|
||||
over := plainRender(t, m.View())
|
||||
|
||||
if !strings.Contains(over, "Keyboard Shortcuts") {
|
||||
t.Fatalf("help overlay not rendered:\n%s", over)
|
||||
}
|
||||
// Chrome that renders in the baseline (and sits outside the centered overlay
|
||||
// box) must survive behind the overlay. The full-screen replace showed none
|
||||
// of it.
|
||||
for _, marker := range []string{"gpt-4o", "describe a task"} {
|
||||
if !strings.Contains(base, marker) {
|
||||
t.Fatalf("precondition: baseline chat should contain %q:\n%s", marker, base)
|
||||
}
|
||||
if !strings.Contains(over, marker) {
|
||||
t.Fatalf("#419: help replaced the chat instead of overlaying it; %q is gone:\n%s", marker, over)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A populated transcript row also survives behind the overlay (peeking out to
|
||||
// the left of the centered box), proving the chat body — not just the chrome —
|
||||
// is composited under the overlay rather than discarded.
|
||||
func TestHelpOverlayKeepsTranscriptBodyBehindIt(t *testing.T) {
|
||||
m := newModel(context.Background(), Options{ModelName: "gpt-4o"})
|
||||
m.width = 120
|
||||
m.height = 40
|
||||
m.altScreen = true
|
||||
m.transcript = appendTranscriptRow(m.transcript, transcriptRow{kind: rowUser, text: "hello there this is a chat line"})
|
||||
m.helpOverlay = true
|
||||
|
||||
view := plainRender(t, m.View())
|
||||
if !strings.Contains(view, "Keyboard Shortcuts") {
|
||||
t.Fatalf("help overlay not rendered:\n%s", view)
|
||||
}
|
||||
// The start of the transcript line peeks to the left of the centered box.
|
||||
if !strings.Contains(view, "hello") {
|
||||
t.Fatalf("#419: transcript body was replaced by the help overlay:\n%s", view)
|
||||
}
|
||||
}
|
||||
@@ -1068,7 +1068,7 @@ func (m model) updateModel(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m = m.disarmCancelConfirmation()
|
||||
}
|
||||
// The `?` help overlay is modal: `?`, Esc, q, or Enter close it; every
|
||||
// other key is swallowed so nothing types into the hidden composer.
|
||||
// other key is swallowed so nothing types into the composer behind it.
|
||||
if m.helpOverlay {
|
||||
if keyText(msg) == "?" || keyText(msg) == "q" || keyIs(msg, tea.KeyEsc) || keyIs(msg, tea.KeyEnter) || keyCtrl(msg, 'c') {
|
||||
m.helpOverlay = false
|
||||
@@ -2118,8 +2118,6 @@ func (m model) View() tea.View {
|
||||
var content string
|
||||
if m.setup.visible {
|
||||
content = m.setupView(chatWidth(m.width))
|
||||
} else if m.helpOverlay {
|
||||
content = m.renderKeybindingHelpOverlay(chatWidth(m.width), m.height)
|
||||
} else if m.transcriptDetailed {
|
||||
content = m.detailedTranscriptView()
|
||||
} else {
|
||||
@@ -2194,6 +2192,7 @@ func (m model) transcriptView() string {
|
||||
return body + footer
|
||||
}
|
||||
|
||||
helpOverlay := m.keybindingHelpOverlay(width)
|
||||
suggestionOverlay := m.suggestionOverlay(width)
|
||||
providerOverlay := m.providerWizardOverlay(width)
|
||||
mcpAddOverlay := m.mcpAddWizardOverlay(width)
|
||||
@@ -2201,6 +2200,8 @@ func (m model) transcriptView() string {
|
||||
pickerOverlay := m.pickerOverlay(width)
|
||||
viewportOverlay := ""
|
||||
switch {
|
||||
case helpOverlay != "":
|
||||
viewportOverlay = helpOverlay
|
||||
case providerOverlay != "":
|
||||
viewportOverlay = providerOverlay
|
||||
case mcpAddOverlay != "":
|
||||
@@ -2262,6 +2263,11 @@ func (m model) twoColumnTranscriptView() string {
|
||||
if m.transcriptEmpty() && !m.pending {
|
||||
overlayForViewport = ""
|
||||
}
|
||||
// The `?` help overlay composites over the chat like the pickers — set it
|
||||
// after the empty-transcript clear so a user-toggled overlay always shows.
|
||||
if helpOverlay := m.keybindingHelpOverlay(width); helpOverlay != "" {
|
||||
overlayForViewport = helpOverlay
|
||||
}
|
||||
|
||||
header := m.pinnedTitleBar(width)
|
||||
chatBlock := viewLines(m.scrollableTranscriptItemsView(header, bodyItems, footer, width, overlayForViewport))
|
||||
|
||||
Reference in new issue
Block a user