Fixed tab naming to correspond to the web client. (#1780)
This commit is contained in:
parent
07d6316fba
commit
d009fcca09
13 changed files with 134 additions and 51 deletions
|
|
@ -76,8 +76,9 @@ The TUI provides terminal-native tabs and panes for interacting with your MCP se
|
||||||
- **Resources**: Browse and read resources exposed by the server.
|
- **Resources**: Browse and read resources exposed by the server.
|
||||||
- **Prompts**: List and test prompts.
|
- **Prompts**: List and test prompts.
|
||||||
- **Tools**: View available tools and execute them with form-like inputs.
|
- **Tools**: View available tools and execute them with form-like inputs.
|
||||||
- **History**: View the request and response history of your interactions.
|
- **Protocol**: View JSON-RPC request/response/notification history (matches the web Protocol monitor).
|
||||||
- **Console**: View the direct stdout/stderr and diagnostic logging of the connected server.
|
- **Network**: View HTTP fetch traffic for SSE / Streamable HTTP servers (matches the web Network monitor).
|
||||||
|
- **Console**: View stdio stderr from the connected server process (matches the web Console monitor).
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -686,7 +686,7 @@ describe("App (status, layout, modals)", () => {
|
||||||
h.ctrl.fetchRequests = [{ ...errorRequest, responseStatus: 401 }];
|
h.ctrl.fetchRequests = [{ ...errorRequest, responseStatus: 401 }];
|
||||||
const r = await mount(oneHttp());
|
const r = await mount(oneHttp());
|
||||||
await expectFrame(r, "error");
|
await expectFrame(r, "error");
|
||||||
await expectFrame(r, "HTTP Requests (1)");
|
await expectFrame(r, "Network (1)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates dimensions when the terminal resizes", async () => {
|
it("updates dimensions when the terminal resizes", async () => {
|
||||||
|
|
@ -752,7 +752,7 @@ describe("App (status, layout, modals)", () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.prompts = [promptWithArgs];
|
h.ctrl.prompts = [promptWithArgs];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["p", TAB, ENTER]);
|
await press(r, ["m", TAB, ENTER]);
|
||||||
await expectFrame(r, "MOCK_FORM");
|
await expectFrame(r, "MOCK_FORM");
|
||||||
await press(r, [ESC]);
|
await press(r, [ESC]);
|
||||||
expect(r.lastFrame() ?? "").not.toContain("MOCK_FORM");
|
expect(r.lastFrame() ?? "").not.toContain("MOCK_FORM");
|
||||||
|
|
@ -762,7 +762,7 @@ describe("App (status, layout, modals)", () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.prompts = [promptWithArgs];
|
h.ctrl.prompts = [promptWithArgs];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["p", TAB, TAB, "+"]);
|
await press(r, ["m", TAB, TAB, "+"]);
|
||||||
const f = r.lastFrame() ?? "";
|
const f = r.lastFrame() ?? "";
|
||||||
expect(f).toContain("Arguments:");
|
expect(f).toContain("Arguments:");
|
||||||
expect(f).toContain("Full JSON:");
|
expect(f).toContain("Full JSON:");
|
||||||
|
|
@ -772,7 +772,7 @@ describe("App (status, layout, modals)", () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.prompts = [promptNoName];
|
h.ctrl.prompts = [promptNoName];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["p", TAB, TAB, "+"]);
|
await press(r, ["m", TAB, TAB, "+"]);
|
||||||
const f = r.lastFrame() ?? "";
|
const f = r.lastFrame() ?? "";
|
||||||
expect(f).toContain("Prompt: Unknown");
|
expect(f).toContain("Prompt: Unknown");
|
||||||
expect(f).not.toContain("Arguments:");
|
expect(f).not.toContain("Arguments:");
|
||||||
|
|
@ -781,7 +781,7 @@ describe("App (status, layout, modals)", () => {
|
||||||
it("opens message details for a request message (with response)", async () => {
|
it("opens message details for a request message (with response)", async () => {
|
||||||
h.ctrl.messages = [reqMessage];
|
h.ctrl.messages = [reqMessage];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["m", TAB, TAB, "+"]);
|
await press(r, ["p", TAB, TAB, "+"]);
|
||||||
const f = r.lastFrame() ?? "";
|
const f = r.lastFrame() ?? "";
|
||||||
expect(f).toContain("Direction: request");
|
expect(f).toContain("Direction: request");
|
||||||
expect(f).toContain("Response:");
|
expect(f).toContain("Response:");
|
||||||
|
|
@ -790,14 +790,14 @@ describe("App (status, layout, modals)", () => {
|
||||||
it("opens message details for a notification message", async () => {
|
it("opens message details for a notification message", async () => {
|
||||||
h.ctrl.messages = [notifMessage];
|
h.ctrl.messages = [notifMessage];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["m", TAB, TAB, "+"]);
|
await press(r, ["p", TAB, TAB, "+"]);
|
||||||
await expectFrame(r, "Notification:");
|
await expectFrame(r, "Notification:");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens message details for a response message", async () => {
|
it("opens message details for a response message", async () => {
|
||||||
h.ctrl.messages = [respMessage];
|
h.ctrl.messages = [respMessage];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["m", TAB, TAB, "+"]);
|
await press(r, ["p", TAB, TAB, "+"]);
|
||||||
await expectFrame(r, "Response:");
|
await expectFrame(r, "Response:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -805,7 +805,7 @@ describe("App (status, layout, modals)", () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.fetchRequests = [bareRequest];
|
h.ctrl.fetchRequests = [bareRequest];
|
||||||
const r = await mount(oneHttp());
|
const r = await mount(oneHttp());
|
||||||
await press(r, ["h", TAB, TAB, "+"]);
|
await press(r, ["n", TAB, TAB, "+"]);
|
||||||
await expectFrame(r, "Request Headers:");
|
await expectFrame(r, "Request Headers:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -825,11 +825,11 @@ describe("App (status, layout, modals)", () => {
|
||||||
expect(h.disconnect).toHaveBeenCalled();
|
expect(h.disconnect).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the HTTP requests tab and opens full request details", async () => {
|
it("renders the Network tab and opens full request details", async () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.fetchRequests = [fullRequest];
|
h.ctrl.fetchRequests = [fullRequest];
|
||||||
const r = await mount(oneHttp());
|
const r = await mount(oneHttp());
|
||||||
await press(r, ["h", TAB, TAB, "+"]);
|
await press(r, ["n", TAB, TAB, "+"]);
|
||||||
const f = r.lastFrame() ?? "";
|
const f = r.lastFrame() ?? "";
|
||||||
expect(f).toContain("Request Headers:");
|
expect(f).toContain("Request Headers:");
|
||||||
expect(f).toContain("Status: 200");
|
expect(f).toContain("Status: 200");
|
||||||
|
|
@ -839,17 +839,17 @@ describe("App (status, layout, modals)", () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.fetchRequests = [errorRequest];
|
h.ctrl.fetchRequests = [errorRequest];
|
||||||
const r = await mount(oneHttp());
|
const r = await mount(oneHttp());
|
||||||
await press(r, ["h", TAB, TAB, "+"]);
|
await press(r, ["n", TAB, TAB, "+"]);
|
||||||
await expectFrame(r, "Error: boom");
|
await expectFrame(r, "Error: boom");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the Logging tab for a stdio server", async () => {
|
it("renders the Console tab for a stdio server", async () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.stderrLogs = [stderrLog];
|
h.ctrl.stderrLogs = [stderrLog];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["l"]);
|
await press(r, ["o"]);
|
||||||
const f = r.lastFrame() ?? "";
|
const f = r.lastFrame() ?? "";
|
||||||
expect(f).toContain("Logging (1)");
|
expect(f).toContain("Console (1)");
|
||||||
expect(f).toContain("log line");
|
expect(f).toContain("log line");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -913,20 +913,20 @@ describe("App (input handling, focus, effects)", () => {
|
||||||
it("cycles focus order through the messages tab panes", async () => {
|
it("cycles focus order through the messages tab panes", async () => {
|
||||||
h.ctrl.messages = [reqMessage];
|
h.ctrl.messages = [reqMessage];
|
||||||
const r = await mount(oneStdio());
|
const r = await mount(oneStdio());
|
||||||
await press(r, ["m"]);
|
await press(r, ["p"]);
|
||||||
await press(r, [TAB, TAB, TAB, TAB]); // forward through messages focusOrder
|
await press(r, [TAB, TAB, TAB, TAB]); // forward through messages focusOrder
|
||||||
await press(r, [STAB, STAB]); // reverse
|
await press(r, [STAB, STAB]); // reverse
|
||||||
await expectFrame(r, "Messages");
|
await expectFrame(r, "Protocol");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("cycles focus order through the requests tab panes", async () => {
|
it("cycles focus order through the requests tab panes", async () => {
|
||||||
h.ctrl.status = "connected";
|
h.ctrl.status = "connected";
|
||||||
h.ctrl.fetchRequests = [fullRequest];
|
h.ctrl.fetchRequests = [fullRequest];
|
||||||
const r = await mount(oneHttp());
|
const r = await mount(oneHttp());
|
||||||
await press(r, ["h"]);
|
await press(r, ["n"]);
|
||||||
await press(r, [TAB, TAB, TAB, TAB]);
|
await press(r, [TAB, TAB, TAB, TAB]);
|
||||||
await press(r, [STAB, STAB]);
|
await press(r, [STAB, STAB]);
|
||||||
await expectFrame(r, "Requests");
|
await expectFrame(r, "Network");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches away from the Auth tab when selecting a non-OAuth server", async () => {
|
it("switches away from the Auth tab when selecting a non-OAuth server", async () => {
|
||||||
|
|
@ -937,11 +937,11 @@ describe("App (input handling, focus, effects)", () => {
|
||||||
expect(r.lastFrame() ?? "").not.toContain("No OAuth information yet");
|
expect(r.lastFrame() ?? "").not.toContain("No OAuth information yet");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches away from the Logging tab when selecting a non-stdio server", async () => {
|
it("switches away from the Console tab when selecting a non-stdio server", async () => {
|
||||||
const r = await mount(stdioThenHttp());
|
const r = await mount(stdioThenHttp());
|
||||||
await press(r, ["l"]); // Logging tab (stdio)
|
await press(r, ["o"]); // Console tab (stdio)
|
||||||
await press(r, [STAB]); // tabs -> serverList
|
await press(r, [STAB]); // tabs -> serverList
|
||||||
await press(r, [DOWN]); // select the http server -> effect leaves Logging
|
await press(r, [DOWN]); // select the http server -> effect leaves Console
|
||||||
await expectFrame(r, "Server Configuration");
|
await expectFrame(r, "Server Configuration");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ describe("HistoryTab", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Messages (0)");
|
expect(frame).toContain("Protocol (0)");
|
||||||
expect(frame).toContain("No messages");
|
expect(frame).toContain("No messages");
|
||||||
expect(frame).toContain("Select a message to view details");
|
expect(frame).toContain("Select a message to view details");
|
||||||
expect(onCountChange).toHaveBeenCalledWith(0);
|
expect(onCountChange).toHaveBeenCalledWith(0);
|
||||||
|
|
@ -115,7 +115,7 @@ describe("HistoryTab", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Messages (7)");
|
expect(frame).toContain("Protocol (7)");
|
||||||
// request with response → "✓"; pending request → "..."
|
// request with response → "✓"; pending request → "..."
|
||||||
expect(frame).toContain("→ tools/list ✓");
|
expect(frame).toContain("→ tools/list ✓");
|
||||||
expect(frame).toContain("→ tools/call ...");
|
expect(frame).toContain("→ tools/call ...");
|
||||||
|
|
@ -226,7 +226,7 @@ describe("HistoryTab", () => {
|
||||||
// up to move back toward the top
|
// up to move back toward the top
|
||||||
stdin.write(UP);
|
stdin.write(UP);
|
||||||
await tick();
|
await tick();
|
||||||
expect(lastFrame() ?? "").toContain("Messages (7)");
|
expect(lastFrame() ?? "").toContain("Protocol (7)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles details-pane scrolling, footer, and zoom shortcut", async () => {
|
it("handles details-pane scrolling, footer, and zoom shortcut", async () => {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ describe("NotificationsTab", () => {
|
||||||
<NotificationsTab stderrLogs={[]} width={80} height={20} />,
|
<NotificationsTab stderrLogs={[]} width={80} height={20} />,
|
||||||
);
|
);
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Logging (0)");
|
expect(frame).toContain("Console (0)");
|
||||||
expect(frame).toContain("No stderr output yet");
|
expect(frame).toContain("No stderr output yet");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ describe("NotificationsTab", () => {
|
||||||
<NotificationsTab stderrLogs={logs} width={80} height={20} />,
|
<NotificationsTab stderrLogs={logs} width={80} height={20} />,
|
||||||
);
|
);
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Logging (2)");
|
expect(frame).toContain("Console (2)");
|
||||||
expect(frame).toContain("first error");
|
expect(frame).toContain("first error");
|
||||||
expect(frame).toContain("second error");
|
expect(frame).toContain("second error");
|
||||||
expect(frame).not.toContain("No stderr output yet");
|
expect(frame).not.toContain("No stderr output yet");
|
||||||
|
|
@ -134,7 +134,7 @@ describe("NotificationsTab", () => {
|
||||||
const { lastFrame, stdin } = render(
|
const { lastFrame, stdin } = render(
|
||||||
<NotificationsTab stderrLogs={logs} width={80} height={20} focused />,
|
<NotificationsTab stderrLogs={logs} width={80} height={20} focused />,
|
||||||
);
|
);
|
||||||
expect(lastFrame() ?? "").toContain("Logging (2)");
|
expect(lastFrame() ?? "").toContain("Console (2)");
|
||||||
|
|
||||||
// Drive every useInput branch
|
// Drive every useInput branch
|
||||||
stdin.write(UP);
|
stdin.write(UP);
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ describe("RequestsTab", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Requests (0)");
|
expect(frame).toContain("Network (0)");
|
||||||
expect(frame).toContain("No requests");
|
expect(frame).toContain("No requests");
|
||||||
expect(frame).toContain("Select a request to view details");
|
expect(frame).toContain("Select a request to view details");
|
||||||
expect(onCountChange).toHaveBeenCalledWith(0);
|
expect(onCountChange).toHaveBeenCalledWith(0);
|
||||||
|
|
@ -107,7 +107,7 @@ describe("RequestsTab", () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<RequestsTab serverName="srv" requests={[]} width={120} height={30} />,
|
<RequestsTab serverName="srv" requests={[]} width={120} height={30} />,
|
||||||
);
|
);
|
||||||
expect(lastFrame() ?? "").toContain("Requests (0)");
|
expect(lastFrame() ?? "").toContain("Network (0)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the list with status colors, labels and durations", () => {
|
it("renders the list with status colors, labels and durations", () => {
|
||||||
|
|
@ -125,7 +125,7 @@ describe("RequestsTab", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Requests (4)");
|
expect(frame).toContain("Network (4)");
|
||||||
// category labels
|
// category labels
|
||||||
expect(frame).toContain("AUTH");
|
expect(frame).toContain("AUTH");
|
||||||
expect(frame).toContain("MCP");
|
expect(frame).toContain("MCP");
|
||||||
|
|
@ -272,7 +272,7 @@ describe("RequestsTab", () => {
|
||||||
// up moves back toward the top
|
// up moves back toward the top
|
||||||
stdin.write(UP);
|
stdin.write(UP);
|
||||||
await tick();
|
await tick();
|
||||||
expect(lastFrame() ?? "").toContain("Requests (8)");
|
expect(lastFrame() ?? "").toContain("Network (8)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clamps at the bottom when paging past the end", async () => {
|
it("clamps at the bottom when paging past the end", async () => {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,10 @@ describe("Tabs", () => {
|
||||||
const frame = lastFrame() ?? "";
|
const frame = lastFrame() ?? "";
|
||||||
expect(frame).toContain("Info");
|
expect(frame).toContain("Info");
|
||||||
expect(frame).toContain("Auth");
|
expect(frame).toContain("Auth");
|
||||||
expect(frame).toContain("Logging");
|
expect(frame).toContain("Console");
|
||||||
// requests defaults to hidden
|
expect(frame).toContain("Protocol");
|
||||||
expect(frame).not.toContain("HTTP Requests");
|
// Network defaults to hidden
|
||||||
|
expect(frame).not.toContain("Network");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides the auth tab when showAuth is false", () => {
|
it("hides the auth tab when showAuth is false", () => {
|
||||||
|
|
@ -34,7 +35,7 @@ describe("Tabs", () => {
|
||||||
showLogging={false}
|
showLogging={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame() ?? "").not.toContain("Logging");
|
expect(lastFrame() ?? "").not.toContain("Console");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the requests tab when showRequests is true", () => {
|
it("shows the requests tab when showRequests is true", () => {
|
||||||
|
|
@ -46,7 +47,7 @@ describe("Tabs", () => {
|
||||||
showRequests={true}
|
showRequests={true}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame() ?? "").toContain("HTTP Requests");
|
expect(lastFrame() ?? "").toContain("Network");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks the active tab with the ▶ marker", () => {
|
it("marks the active tab with the ▶ marker", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { tabs } from "../src/components/tabsConfig.js";
|
import { tabs } from "../src/components/tabsConfig.js";
|
||||||
|
import { splitLabelAtAccelerator } from "../src/components/Tabs.js";
|
||||||
|
|
||||||
describe("TUI", () => {
|
describe("TUI", () => {
|
||||||
it("exports tabs with expected shape", () => {
|
it("exports tabs with expected shape", () => {
|
||||||
|
|
@ -18,4 +19,52 @@ describe("TUI", () => {
|
||||||
expect(info?.label).toBe("Info");
|
expect(info?.label).toBe("Info");
|
||||||
expect(info?.accelerator).toBe("i");
|
expect(info?.accelerator).toBe("i");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses web-aligned monitor tab labels with unique in-label accelerators", () => {
|
||||||
|
expect(tabs.find((t) => t.id === "messages")).toMatchObject({
|
||||||
|
label: "Protocol",
|
||||||
|
accelerator: "p",
|
||||||
|
});
|
||||||
|
expect(tabs.find((t) => t.id === "requests")).toMatchObject({
|
||||||
|
label: "Network",
|
||||||
|
accelerator: "n",
|
||||||
|
});
|
||||||
|
expect(tabs.find((t) => t.id === "logging")).toMatchObject({
|
||||||
|
label: "Console",
|
||||||
|
accelerator: "o",
|
||||||
|
});
|
||||||
|
expect(tabs.find((t) => t.id === "prompts")).toMatchObject({
|
||||||
|
label: "Prompts",
|
||||||
|
accelerator: "m",
|
||||||
|
});
|
||||||
|
|
||||||
|
const accelerators = tabs.map((t) => t.accelerator);
|
||||||
|
expect(new Set(accelerators).size).toBe(accelerators.length);
|
||||||
|
for (const tab of tabs) {
|
||||||
|
expect(tab.label.toLowerCase()).toContain(tab.accelerator.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits labels so the accelerator letter can be underlined mid-word", () => {
|
||||||
|
expect(splitLabelAtAccelerator("Prompts", "m")).toEqual({
|
||||||
|
before: "Pro",
|
||||||
|
accel: "m",
|
||||||
|
after: "pts",
|
||||||
|
});
|
||||||
|
expect(splitLabelAtAccelerator("Console", "o")).toEqual({
|
||||||
|
before: "C",
|
||||||
|
accel: "o",
|
||||||
|
after: "nsole",
|
||||||
|
});
|
||||||
|
expect(splitLabelAtAccelerator("Protocol", "p")).toEqual({
|
||||||
|
before: "",
|
||||||
|
accel: "P",
|
||||||
|
after: "rotocol",
|
||||||
|
});
|
||||||
|
expect(splitLabelAtAccelerator("Info", "z")).toEqual({
|
||||||
|
before: "",
|
||||||
|
accel: "I",
|
||||||
|
after: "nfo",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1289,7 +1289,7 @@ function App({
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab switching with accelerator keys (first character of tab name)
|
// Tab switching with accelerator keys (letter from the tab label)
|
||||||
const showAuthTab =
|
const showAuthTab =
|
||||||
!!selectedServer &&
|
!!selectedServer &&
|
||||||
!!selectedServerConfig &&
|
!!selectedServerConfig &&
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,11 @@ export function HistoryTab({
|
||||||
bold
|
bold
|
||||||
backgroundColor={focusedPane === "messages" ? "yellow" : undefined}
|
backgroundColor={focusedPane === "messages" ? "yellow" : undefined}
|
||||||
>
|
>
|
||||||
Messages ({messages.length})
|
Protocol ({messages.length})
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Messages list */}
|
{/* Protocol message list */}
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<Box paddingY={1}>
|
<Box paddingY={1}>
|
||||||
<Text dimColor>No messages</Text>
|
<Text dimColor>No messages</Text>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export function NotificationsTab({
|
||||||
<Box width={width} height={height} flexDirection="column" paddingX={1}>
|
<Box width={width} height={height} flexDirection="column" paddingX={1}>
|
||||||
<Box paddingY={1} flexShrink={0}>
|
<Box paddingY={1} flexShrink={0}>
|
||||||
<Text bold backgroundColor={focused ? "yellow" : undefined}>
|
<Text bold backgroundColor={focused ? "yellow" : undefined}>
|
||||||
Logging ({stderrLogs.length})
|
Console ({stderrLogs.length})
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{stderrLogs.length === 0 ? (
|
{stderrLogs.length === 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export function RequestsTab({
|
||||||
bold
|
bold
|
||||||
backgroundColor={focusedPane === "requests" ? "yellow" : undefined}
|
backgroundColor={focusedPane === "requests" ? "yellow" : undefined}
|
||||||
>
|
>
|
||||||
Requests ({requests.length})
|
Network ({requests.length})
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,25 @@ import React from "react";
|
||||||
import { Box, Text } from "ink";
|
import { Box, Text } from "ink";
|
||||||
import { type TabType, tabs } from "./tabsConfig.js";
|
import { type TabType, tabs } from "./tabsConfig.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a tab label so the accelerator letter can be underlined wherever it
|
||||||
|
* appears (not only the first character — e.g. Pro**m**pts, C**o**nsole).
|
||||||
|
*/
|
||||||
|
export function splitLabelAtAccelerator(
|
||||||
|
label: string,
|
||||||
|
accelerator: string,
|
||||||
|
): { before: string; accel: string; after: string } {
|
||||||
|
const idx = label.toLowerCase().indexOf(accelerator.toLowerCase());
|
||||||
|
if (idx < 0) {
|
||||||
|
return { before: "", accel: label.slice(0, 1), after: label.slice(1) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
before: label.slice(0, idx),
|
||||||
|
accel: label.slice(idx, idx + 1),
|
||||||
|
after: label.slice(idx + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface TabsProps {
|
interface TabsProps {
|
||||||
activeTab: TabType;
|
activeTab: TabType;
|
||||||
onTabChange: (tab: TabType) => void;
|
onTabChange: (tab: TabType) => void;
|
||||||
|
|
@ -60,8 +79,10 @@ export function Tabs({
|
||||||
const isActive = activeTab === tab.id;
|
const isActive = activeTab === tab.id;
|
||||||
const count = counts[tab.id];
|
const count = counts[tab.id];
|
||||||
const countText = count !== undefined ? ` (${count})` : "";
|
const countText = count !== undefined ? ` (${count})` : "";
|
||||||
const firstChar = tab.label[0];
|
const { before, accel, after } = splitLabelAtAccelerator(
|
||||||
const restOfLabel = tab.label.slice(1);
|
tab.label,
|
||||||
|
tab.accelerator,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={tab.id} flexShrink={0}>
|
<Box key={tab.id} flexShrink={0}>
|
||||||
|
|
@ -73,8 +94,9 @@ export function Tabs({
|
||||||
backgroundColor={isActive && focused ? "yellow" : undefined}
|
backgroundColor={isActive && focused ? "yellow" : undefined}
|
||||||
>
|
>
|
||||||
{isActive ? "▶ " : " "}
|
{isActive ? "▶ " : " "}
|
||||||
<Text underline>{firstChar}</Text>
|
{before}
|
||||||
{restOfLabel}
|
<Text underline>{accel}</Text>
|
||||||
|
{after}
|
||||||
{countText}
|
{countText}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,23 @@ export type TabType =
|
||||||
| "requests"
|
| "requests"
|
||||||
| "logging";
|
| "logging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab bar labels + single-letter accelerators.
|
||||||
|
*
|
||||||
|
* Accelerators must be unique and appear in the label. Prefer the first letter;
|
||||||
|
* when that conflicts (Protocol vs Prompts both want `p`, Console vs Connect's
|
||||||
|
* global `c`), pick the earliest remaining letter in the word.
|
||||||
|
*
|
||||||
|
* Connect (`c`) / Disconnect (`d`) are global actions, not tab accelerators —
|
||||||
|
* Console therefore uses `o` (C**o**nsole).
|
||||||
|
*/
|
||||||
export const tabs: { id: TabType; label: string; accelerator: string }[] = [
|
export const tabs: { id: TabType; label: string; accelerator: string }[] = [
|
||||||
{ id: "info", label: "Info", accelerator: "i" },
|
{ id: "info", label: "Info", accelerator: "i" },
|
||||||
{ id: "auth", label: "Auth", accelerator: "a" },
|
{ id: "auth", label: "Auth", accelerator: "a" },
|
||||||
{ id: "resources", label: "Resources", accelerator: "r" },
|
{ id: "resources", label: "Resources", accelerator: "r" },
|
||||||
{ id: "prompts", label: "Prompts", accelerator: "p" },
|
{ id: "prompts", label: "Prompts", accelerator: "m" },
|
||||||
{ id: "tools", label: "Tools", accelerator: "t" },
|
{ id: "tools", label: "Tools", accelerator: "t" },
|
||||||
{ id: "messages", label: "Messages", accelerator: "m" },
|
{ id: "messages", label: "Protocol", accelerator: "p" },
|
||||||
{ id: "requests", label: "HTTP Requests", accelerator: "h" },
|
{ id: "requests", label: "Network", accelerator: "n" },
|
||||||
{ id: "logging", label: "Logging", accelerator: "l" },
|
{ id: "logging", label: "Console", accelerator: "o" },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue