Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .github/workflows/rogue.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Rogue

on:
pull_request_target:
pull_request:
push:
branches:
- main
Expand All @@ -11,17 +11,11 @@ jobs:
rogue_sanity:
runs-on: ubuntu-latest
timeout-minutes: 15
environment: rogue-sanity-ci-secrets
# Only run for PRs from within the same repository, not from forks
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
# 1. Checkout the actual PR commit, not just the base branch
ref: ${{ github.event.pull_request.head.sha }}
# 2. DO NOT expose the GITHUB_TOKEN write-permissions to the untrusted code
# This is essential to prevent untrusted code from exfiltrating secrets
# by manipulating the repository itself.
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ dist/
.adk/
**/.rogue
bin/

# Go TUI
packages/tui/cmd/rogue/__debug*
8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@
],
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Rogue TUI",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/packages/tui/cmd/rogue/main.go",
"console": "integratedTerminal"
},
{
"name": "Rogue AIO",
"type": "debugpy",
Expand Down
2 changes: 1 addition & 1 deletion examples/mcp/tshirt_store_langgraph_mcp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"--transport",
"transport",
default="streamable-http",
choices=["streamable-http", "sse"],
type=click.Choice(["streamable-http", "sse"]),
help="Transport to use for the mcp server",
)
def main(host: str, port: int, transport: Literal["streamable-http", "sse"]) -> None:
Expand Down
92 changes: 92 additions & 0 deletions packages/sdk/src/qualifire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { EvaluationResult, ReportSummaryRequest, StructuredSummary } from "./types";

interface QualifireReportPayload {
job_id: string;
evaluations: EvaluationResult;
structured: StructuredSummary | null;
deep_test: boolean;
start_time: string;
judge_model: string | null;
}
Comment on lines +3 to +10
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Type mismatch: deep_test and start_time should be nullable.

The QualifireReportPayload interface defines deep_test: boolean and start_time: string as required fields. However, the source fields in ReportSummaryRequest are optional (deepTest?: boolean, startTime?: string). This creates a type mismatch where undefined values can be assigned to non-nullable fields in the payload, violating the type contract and potentially causing API errors.

Apply this diff to make the fields nullable:

 interface QualifireReportPayload {
   job_id: string;
   evaluations: EvaluationResult;
   structured: StructuredSummary | null;
-  deep_test: boolean;
-  start_time: string;
+  deep_test: boolean | null;
+  start_time: string | null;
   judge_model: string | null;
 }

Then update the mapping in convertWithStructuredSummary:

   return {
     job_id: request.job_id,
     evaluations: evaluationResults,
     structured: request.structuredSummary || null,
-    deep_test: request.deepTest,
-    start_time: request.startTime,
+    deep_test: request.deepTest ?? null,
+    start_time: request.startTime ?? null,
     judge_model: request.judgeModel || null,
   };

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In packages/sdk/src/qualifire.ts around lines 3 to 10, the
QualifireReportPayload currently declares deep_test: boolean and start_time:
string but the source ReportSummaryRequest has deepTest?: boolean and
startTime?: string, causing a type mismatch; change the interface to make
deep_test: boolean | null and start_time: string | null, and then update
convertWithStructuredSummary to map optional fields defensively (deep_test:
source.deepTest ?? null and start_time: source.startTime ?? null) so undefined
from the source becomes null in the payload.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ignorant05 I think this comment is correct

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


export interface QualifireClientOptions {
logger?: (message: string) => void;
}

export class QualifireClient {
private static convertWithStructuredSummary(
evaluationResults: EvaluationResult,
request: ReportSummaryRequest
): QualifireReportPayload {
return {
job_id: request.job_id,
evaluations: evaluationResults,
structured: request.structuredSummary || null,
deep_test: request.deepTest ?? false,
start_time: request.startTime ?? new Date().toISOString(),
judge_model: request.judgeModel || null,
};
}

/**
* Reports evaluation summary to Qualifire.
*
* @param evaluationResults - The evaluation results to report
* @param request - Configuration including Qualifire URL, API key, and metadata
* @throws {Error} If the API request fails or returns a non-2xx status
* @returns A promise that resolves when the report is successfully submitted
*/
public static async reportSummaryToQualifire(
evaluationResults: EvaluationResult,
request: ReportSummaryRequest,
options?: QualifireClientOptions
): Promise<void> {
options?.logger?.("Reporting summary to Qualifire");

const apiKey = request.qualifireApiKey;
const baseUrl = request.qualifireUrl ?? "https://api.qualifire.com";
const endpoint = `${baseUrl}/api/v1/evaluations`;

if (!apiKey) {
throw new Error("qualifireApiKey is required but was undefined");
}

if (!baseUrl || baseUrl === "undefined") {
throw new Error("Invalid qualifireUrl provided");
}
Comment on lines 46 to 56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Design mismatch with PR objective: route via Rogue server, not direct Qualifire.

This class posts directly to Qualifire (/api/v1/evaluations). The stated goal is to mimic the Python SDK and pass the Qualifire API key to the Rogue server’s summary endpoint. Please change this to call the Rogue server endpoint and forward the key, or gate direct reporting behind an explicit opt-in and default to server routing for this PR.

Would you run the script below to locate the server summary endpoint and confirm the expected header so we can align the client?


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find Rogue summary endpoint and Qualifire service usage =="
fd -a llm.py rogue/server | xargs -I{} rg -n -C3 -e 'router\.(post|get|put)\(' -e 'summary' {}
rg -n -C3 'qualifire' rogue/server || true
rg -n -C3 'X-Qualifire' rogue/server || true

Length of output: 8186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check SDK qualifire client implementation =="
fd -a "qualifire.ts|qualifier.ts" packages/sdk/src

echo ""
echo "== Find request models for server /summary endpoint =="
rg -n -A10 "class SummaryGenerationRequest|SummaryGenerationRequest =" rogue/server --type py

echo ""
echo "== Check what auth/headers are expected by Rogue /summary endpoint =="
rg -n -B5 -A15 "@router.post\(\s*\"/summary\"" rogue/server --type py

Length of output: 152



🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SDK qualifire.ts current implementation =="
cat /home/jailuser/git/packages/sdk/src/qualifire.ts

echo ""
echo "== Rogue server request models =="
fd -a "models.py|schemas.py|types.py" rogue/server/api rogue/server/schemas 2>/dev/null | head -5 | xargs -I{} sh -c 'echo "=== {} ===" && head -100 {}'

echo ""
echo "== Search for SummaryGenerationRequest in all Python files =="
rg -n "SummaryGenerationRequest" rogue/server --type py -B2 -A5

Length of output: 3865


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find SummaryGenerationRequest definition =="
rg -n "class SummaryGenerationRequest|SummaryGenerationRequest\s*=" rogue/server -A15 --type py

echo ""
echo "== Check import source for SummaryGenerationRequest =="
rg -n "from.*import.*SummaryGenerationRequest" rogue/server --type py

Length of output: 46


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for Summary in rogue module structure =="
fd -a ".py$" rogue/server | xargs rg -l "SummaryGenerationRequest" | head -10

echo ""
echo "== Find all python files in rogue/server/api =="
fd -a ".py$" rogue/server/api

echo ""
echo "== Check rogue/server/schemas or similar =="
fd -a "schema|model" rogue/server --type d

Length of output: 463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== List models directory =="
fd -a ".py$" rogue/server/models

echo ""
echo "== Search for SummaryGenerationRequest in models =="
rg -n "SummaryGenerationRequest" rogue/server/models -A10

echo ""
echo "== Check rogue/server/api/__init__.py for exports =="
cat rogue/server/api/__init__.py

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check models/__init__.py =="
cat rogue/server/models/__init__.py

echo ""
echo "== Check models/api_format.py =="
head -150 rogue/server/models/api_format.py

echo ""
echo "== Broader search for SummaryGenerationRequest =="
rg -n "SummaryGenerationRequest" rogue/ -A5 | head -50

Length of output: 2608


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check llm.py imports (first 30 lines) =="
head -30 rogue/server/api/llm.py

echo ""
echo "== Search for SummaryGenerationRequest definition anywhere in repo =="
rg -n "class SummaryGenerationRequest|SummaryGenerationRequest.*=" --type py

Length of output: 1043


Design mismatch confirmed: SDK posts directly to Qualifire instead of routing through Rogue server.

The SDK calls ${baseUrl}/api/v1/evaluations directly (packages/sdk/src/qualifire.ts:48). Per the PR objective, it should POST to the Rogue server's /llm/summary endpoint instead, which internally handles Qualifire reporting. This aligns with how the Python SDK works and keeps the server in control of credential forwarding.

πŸ€– Prompt for AI Agents
In packages/sdk/src/qualifire.ts around lines 46 to 56, the SDK currently
constructs and posts directly to Qualifire (`${baseUrl}/api/v1/evaluations`);
change this to post to the Rogue server's `/llm/summary` endpoint instead (use
request.rogueUrl as the base, e.g. `${request.rogueUrl}/llm/summary`, with a
sensible default or raise if missing). Update the request payload to match the
Rogue `/llm/summary` contract and forward any needed Qualifire
credentials/metadata in the body or headers so Rogue can handle reporting to
Qualifire; remove the direct Qualifire endpoint logic and keep validation for
the rogue URL instead of qualifireUrl.


const apiEvaluationResult = this.convertWithStructuredSummary(
evaluationResults,
request
);

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);

try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Qualifire-API-Key": apiKey,
},
body: JSON.stringify(apiEvaluationResult),
signal: controller.signal
});

if (!response.ok) {
const errText = await response.text();
throw new Error(
`Qualifire report failed: ${response.status} ${response.statusText} - ${errText}`
);
}
clearTimeout(timeoutId);
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Qualifire report timed out after 30 seconds');
}
throw error;
}
}
}
17 changes: 17 additions & 0 deletions packages/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ export interface RogueClientConfig {
retries?: number;
}

export interface StructuredSummary {
overall_summary: string,
key_findings: string[],
recommendations: string[],
detailed_breakdown: object[]
}

export interface ReportSummaryRequest {
job_id: string,
qualifireUrl?: string;
qualifireApiKey?: string;
structuredSummary?: StructuredSummary;
deepTest?: boolean;
startTime?: string;
judgeModel?: string;
}

// Event Types for WebSocket
export type WebSocketEventType =
| "job_update"
Expand Down
41 changes: 21 additions & 20 deletions packages/tui/internal/tui/common_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,17 @@ func (m Model) handlePasteMsg(msg tea.PasteMsg) (Model, tea.Cmd) {
}

// Only paste into text fields (Agent URL and Judge Model)
if m.evalState.currentField <= 1 {
switch m.evalState.currentField {
case 0: // Agent URL
// Insert at cursor position
runes := []rune(m.evalState.AgentURL)
m.evalState.AgentURL = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
case 1: // Judge Model
// Insert at cursor position
runes := []rune(m.evalState.JudgeModel)
m.evalState.JudgeModel = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
}
switch m.evalState.currentField {
case 0: // Agent URL
// Insert at cursor position
runes := []rune(m.evalState.AgentURL)
m.evalState.AgentURL = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
case 3: // Judge Model
// Insert at cursor position
runes := []rune(m.evalState.JudgeModel)
m.evalState.JudgeModel = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
}
Comment on lines 50 to 61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Clamp cursorPos on paste to prevent panic.

If cursorPos > len(runes), slicing will panic. Clamp before insertion.

-        // Only paste into text fields (Agent URL and Judge Model)
+        // Only paste into text fields (Agent URL and Judge Model)
         switch m.evalState.currentField {
         case 0: // Agent URL
-            // Insert at cursor position
-            runes := []rune(m.evalState.AgentURL)
+            // Insert at cursor position
+            runes := []rune(m.evalState.AgentURL)
+            if m.evalState.cursorPos > len(runes) {
+                m.evalState.cursorPos = len(runes)
+            }
             m.evalState.AgentURL = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
             m.evalState.cursorPos += len([]rune(cleanText))
         case 3: // Judge Model
-            // Insert at cursor position
-            runes := []rune(m.evalState.JudgeModel)
+            // Insert at cursor position
+            runes := []rune(m.evalState.JudgeModel)
+            if m.evalState.cursorPos > len(runes) {
+                m.evalState.cursorPos = len(runes)
+            }
             m.evalState.JudgeModel = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
             m.evalState.cursorPos += len([]rune(cleanText))
         }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch m.evalState.currentField {
case 0: // Agent URL
// Insert at cursor position
runes := []rune(m.evalState.AgentURL)
m.evalState.AgentURL = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
case 3: // Judge Model
// Insert at cursor position
runes := []rune(m.evalState.JudgeModel)
m.evalState.JudgeModel = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
}
switch m.evalState.currentField {
case 0: // Agent URL
// Insert at cursor position
runes := []rune(m.evalState.AgentURL)
if m.evalState.cursorPos > len(runes) {
m.evalState.cursorPos = len(runes)
}
m.evalState.AgentURL = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
case 3: // Judge Model
// Insert at cursor position
runes := []rune(m.evalState.JudgeModel)
if m.evalState.cursorPos > len(runes) {
m.evalState.cursorPos = len(runes)
}
m.evalState.JudgeModel = string(runes[:m.evalState.cursorPos]) + cleanText + string(runes[m.evalState.cursorPos:])
m.evalState.cursorPos += len([]rune(cleanText))
}
πŸ€– Prompt for AI Agents
In packages/tui/internal/tui/common_controller.go around lines 48 to 59, the
code slices runes using m.evalState.cursorPos which can be > len(runes) and
cause a panic on paste; clamp cursorPos to the valid range before slicing (e.g.,
if cursorPos < 0 set to 0, if cursorPos > len(runes) set to len(runes)), then
perform the insert using the clamped index and update cursorPos by adding the
length of cleanText in runes; apply this fix in both the AgentURL and JudgeModel
cases.

return m, nil
}
Expand Down Expand Up @@ -202,14 +200,17 @@ func (m Model) handleCommandSelectedMsg(msg components.CommandSelectedMsg) (Mode
// Use the configured model in provider/model format
judgeModel = m.config.SelectedProvider + "/" + m.config.SelectedModel
}
// TODO read agent url and protocol .rogue/user_config.json
m.evalState = &EvaluationViewState{
ServerURL: m.config.ServerURL,
AgentURL: "http://localhost:10001",
JudgeModel: judgeModel,
ParallelRuns: 1,
DeepTest: false,
Scenarios: loadScenariosFromWorkdir(),
cursorPos: len([]rune("http://localhost:10001")), // Set cursor to end of Agent URL
ServerURL: m.config.ServerURL,
AgentURL: "http://localhost:10001",
AgentProtocol: ProtocolA2A,
AgentTransport: TransportHTTP,
JudgeModel: judgeModel,
ParallelRuns: 1,
DeepTest: false,
Scenarios: loadScenariosFromWorkdir(),
cursorPos: len([]rune("http://localhost:10001")), // Set cursor to end of Agent URL
}
case "configure_models":
// Open LLM configuration dialog
Expand Down
94 changes: 87 additions & 7 deletions packages/tui/internal/tui/eval_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ func (m Model) renderNewEvaluation() string {

title := titleStyle.Render("πŸ§ͺ New Evaluation")

// Helper function to render a field with inline label and value
renderField := func(fieldIndex int, label, value string) string {
// Helper function to render a text field with inline label and value
renderTextField := func(fieldIndex int, label, value string) string {
active := m.evalState.currentField == fieldIndex

labelStyle := lipgloss.NewStyle().
Expand Down Expand Up @@ -83,8 +83,86 @@ func (m Model) renderNewEvaluation() string {
return fieldContainer.Render(fieldContent)
}

// Helper function to render a dropdown field with indicators
renderDropdownField := func(fieldIndex int, label, value string) string {
active := m.evalState.currentField == fieldIndex

labelStyle := lipgloss.NewStyle().
Foreground(t.TextMuted()).
Background(t.Background()).
Width(20).
Align(lipgloss.Right)

valueStyle := lipgloss.NewStyle().
Foreground(t.Text()).
Background(t.Background()).
Padding(0, 1)

if active {
labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
valueStyle = valueStyle.
Foreground(t.Primary()).
Background(t.Background()).
Bold(true)
// Add dropdown indicators
value = "β—€ " + value + " β–Ά"
}

// Create a full-width container for the field
fieldContainer := lipgloss.NewStyle().
Width(m.width-4).
Background(t.Background()).
Padding(0, 2)

fieldContent := lipgloss.JoinHorizontal(lipgloss.Left,
labelStyle.Render(label),
valueStyle.Render(value),
)

return fieldContainer.Render(fieldContent)
}

// Helper function to render a toggle field
renderToggleField := func(fieldIndex int, label, value string) string {
active := m.evalState.currentField == fieldIndex

labelStyle := lipgloss.NewStyle().
Foreground(t.TextMuted()).
Background(t.Background()).
Width(20).
Align(lipgloss.Right)

valueStyle := lipgloss.NewStyle().
Foreground(t.Text()).
Background(t.Background()).
Padding(0, 1)

if active {
labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
valueStyle = valueStyle.
Foreground(t.Primary()).
Background(t.Background()).
Bold(true)
}

// Create a full-width container for the field
fieldContainer := lipgloss.NewStyle().
Width(m.width-4).
Background(t.Background()).
Padding(0, 2)

fieldContent := lipgloss.JoinHorizontal(lipgloss.Left,
labelStyle.Render(label),
valueStyle.Render(value),
)

return fieldContainer.Render(fieldContent)
}

// Prepare field values
agent := m.evalState.AgentURL
protocol := string(m.evalState.AgentProtocol)
transport := string(m.evalState.AgentTransport)
judge := m.evalState.JudgeModel
deep := "❌"
if m.evalState.DeepTest {
Expand All @@ -93,7 +171,7 @@ func (m Model) renderNewEvaluation() string {

// Helper function to render the start button
renderStartButton := func() string {
active := m.evalState.currentField == 3
active := m.evalState.currentField == 5
var buttonText string

if m.evalSpinner.IsActive() {
Expand Down Expand Up @@ -151,9 +229,11 @@ func (m Model) renderNewEvaluation() string {

// Build the content sections
formSection := lipgloss.JoinVertical(lipgloss.Left,
renderField(0, "Agent URL:", agent),
renderField(1, "Judge LLM:", judge),
renderField(2, "Deep Test:", deep),
renderTextField(0, "Agent URL:", agent),
renderDropdownField(1, "Protocol:", protocol),
renderDropdownField(2, "Transport:", transport),
renderTextField(3, "Judge LLM:", judge),
renderToggleField(4, "Deep Test:", deep),
)

var infoLines []string
Expand All @@ -168,7 +248,7 @@ func (m Model) renderNewEvaluation() string {

buttonSection := renderStartButton()

helpText := helpStyle.Render("t Test Server ↑/↓ switch fields ←/β†’ move cursor Space toggle Enter activate Esc Back")
helpText := helpStyle.Render("t Test Server ↑/↓ switch fields ←/β†’ move cursor/cycle dropdown Space toggle Enter activate Esc Back")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Help text advertises "t Test Server" with no visible key binding.

Either wire a 't' handler or remove/update the hint to avoid confusing users.

Apply one of:

- helpText := helpStyle.Render("t Test Server   ↑/↓ switch fields   ←/β†’ move cursor/cycle dropdown    Space toggle   Enter activate   Esc Back")
+ helpText := helpStyle.Render("↑/↓ switch fields   ←/β†’ move cursor/cycle dropdown    Space toggle   Enter activate   Esc Back")

Or add the actual 't' binding where appropriate.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
helpText := helpStyle.Render("t Test Server ↑/↓ switch fields ←/β†’ move cursor/cycle dropdown Space toggle Enter activate Esc Back")
helpText := helpStyle.Render("↑/↓ switch fields ←/β†’ move cursor/cycle dropdown Space toggle Enter activate Esc Back")
πŸ€– Prompt for AI Agents
In packages/tui/internal/tui/eval_form.go around line 251, the help text shows
"t Test Server" but there is no 't' key handler; either remove or change that
hint or implement the 't' keybinding. Fix by either updating the helpText string
to remove or alter the "t Test Server" entry to reflect actual available keys,
or add a key handler for 't' in the form input/key event handling (register the
't' shortcut, perform the Test Server action, and update UI/state and any
related focus/feedback) and ensure tests/update helpText remain consistent.


// Calculate content area height (excluding title and help)
contentHeight := m.height - 6 // title(3) + help(1) + margins(2)
Expand Down
Loading