Skip to content

Commit fa3ebb8

Browse files
committed
feat: Centralize command input state management with InputStore
1 parent 1f9e38a commit fa3ebb8

File tree

5 files changed

+111
-30
lines changed

5 files changed

+111
-30
lines changed

src/InputStore.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// State
2+
export type InputState = {
3+
text: string;
4+
};
5+
6+
// Action Types
7+
export enum InputActionType {
8+
SetInput = "SET_INPUT",
9+
ClearInput = "CLEAR_INPUT",
10+
}
11+
12+
// Actions
13+
export type InputAction =
14+
| { type: InputActionType.SetInput; data: string }
15+
| { type: InputActionType.ClearInput };
16+
17+
// Store Class
18+
class InputStore {
19+
private state: InputState = { text: "" };
20+
private listeners: Set<() => void> = new Set();
21+
22+
private reducer(state: InputState, action: InputAction): InputState {
23+
switch (action.type) {
24+
case InputActionType.SetInput:
25+
// Only update if the text actually changed
26+
if (state.text !== action.data) {
27+
return { ...state, text: action.data };
28+
}
29+
return state;
30+
case InputActionType.ClearInput:
31+
if (state.text !== "") {
32+
return { ...state, text: "" };
33+
}
34+
return state;
35+
default:
36+
return state;
37+
}
38+
}
39+
40+
dispatch = (action: InputAction) => {
41+
const previousState = this.state;
42+
this.state = this.reducer(this.state, action);
43+
// Only notify listeners if the state actually changed
44+
if (this.state !== previousState) {
45+
this.listeners.forEach((listener) => listener());
46+
}
47+
}
48+
49+
getState(): InputState {
50+
return this.state;
51+
}
52+
53+
subscribe(listener: () => void): () => void {
54+
this.listeners.add(listener);
55+
return () => {
56+
this.listeners.delete(listener);
57+
};
58+
}
59+
}
60+
61+
// Singleton instance
62+
export const inputStore = new InputStore();
63+
64+
// Helper functions for easier dispatching
65+
export const setInputText = (text: string) => {
66+
inputStore.dispatch({ type: InputActionType.SetInput, data: text });
67+
};
68+
69+
export const clearInputText = () => {
70+
inputStore.dispatch({ type: InputActionType.ClearInput });
71+
};

src/client.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -563,18 +563,6 @@ An MCP message consists of three parts: the name of the message, the authenticat
563563
] as GMCPClientMedia;
564564
gmcpClientMedia.stopAllSounds();
565565
}
566-
567-
getInput(): string {
568-
// get what the user has typed so far
569-
return document.getElementById("command-input")?.textContent || "";
570-
}
571-
572-
setInput(text: string) {
573-
// place text in the input field
574-
const input = document.getElementById("command-input");
575-
if (!input) return;
576-
input.textContent = text;
577-
}
578566
}
579567

580568
export default MudClient;

src/components/input.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React, { useState, useRef, useEffect } from "react";
1+
import React, { useRef, useEffect, useCallback } from "react";
22
import { CommandHistory } from "../CommandHistory";
33
import "./input.css";
4+
import { useInputStore } from "../hooks/useInputStore";
5+
import { InputActionType, setInputText, clearInputText } from "../InputStore";
46

57
type SendFunction = (text: string) => void;
68

@@ -13,8 +15,8 @@ const STORAGE_KEY = 'command_history';
1315
const MAX_HISTORY = 1000;
1416

1517
const CommandInput = ({ onSend, inputRef }: Props) => {
16-
const [input, setInput] = useState("");
1718
const commandHistoryRef = useRef(new CommandHistory());
19+
const [inputState, dispatch] = useInputStore();
1820

1921
// Load saved history on component mount
2022
useEffect(() => {
@@ -41,38 +43,40 @@ const CommandInput = ({ onSend, inputRef }: Props) => {
4143
}
4244
};
4345

44-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
46+
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
4547
const commandHistory = commandHistoryRef.current;
48+
const currentInput = inputState.text;
4649

4750
if (e.key === "Enter" && !e.shiftKey) {
4851
e.preventDefault();
4952
handleSend();
5053
} else if (e.key === "ArrowUp") {
5154
e.preventDefault();
52-
const prevCommand = commandHistory.navigateUp(input);
53-
setInput(prevCommand);
55+
const prevCommand = commandHistory.navigateUp(currentInput);
56+
setInputText(prevCommand);
5457
} else if (e.key === "ArrowDown") {
5558
e.preventDefault();
56-
const nextCommand = commandHistory.navigateDown(input);
57-
setInput(nextCommand);
59+
const nextCommand = commandHistory.navigateDown(currentInput);
60+
setInputText(nextCommand);
5861
}
59-
};
62+
}, [inputState.text]);
6063

61-
const handleSend = () => {
62-
if (input.trim()) {
63-
onSend(input);
64-
commandHistoryRef.current.addCommand(input);
64+
const handleSend = useCallback(() => {
65+
const currentInput = inputState.text;
66+
if (currentInput.trim()) {
67+
onSend(currentInput);
68+
commandHistoryRef.current.addCommand(currentInput);
6569
saveHistory();
66-
setInput("");
70+
clearInputText();
6771
inputRef.current?.focus();
6872
}
69-
};
73+
}, [inputState.text, onSend, inputRef]);
7074

7175
return (
7276
<div className="command-input-container">
7377
<textarea
74-
value={input}
75-
onChange={(e) => setInput(e.target.value)}
78+
value={inputState.text}
79+
onChange={(e) => setInputText(e.target.value)}
7680
onKeyDown={handleKeyDown}
7781
id="command-input"
7882
ref={inputRef}

src/gmcp/Client/Keystrokes.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type MudClient from "../../client";
22
import { GMCPMessage, GMCPPackage } from "../package";
3+
import { inputStore, setInputText } from "../../InputStore"; // Import the store and helper
34

45
interface KeyBinding {
56
key: string;
@@ -50,7 +51,8 @@ export class GMCPClientKeystrokes extends GMCPPackage {
5051
const binding = this.findBinding(event);
5152
if (binding) {
5253
event.preventDefault();
53-
const commandInput = this.client.getInput();
54+
// Get input directly from the store
55+
const commandInput = inputStore.getState().text;
5456
const command = this.parseCommand(binding.command, commandInput);
5557
if (binding.autosend) {
5658
this.client.sendCommand(command);
@@ -116,7 +118,8 @@ export class GMCPClientKeystrokes extends GMCPPackage {
116118
}
117119

118120
private placeInInputField(command: string): void {
119-
this.client.setInput(command);
121+
// Use the centralized store action
122+
setInputText(command);
120123
}
121124

122125
public bindKey(data: GMCPMessageClientKeystrokesBind): void {

src/hooks/useInputStore.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useEffect, useReducer } from "react";
2+
import { inputStore, InputState, InputAction } from "../InputStore";
3+
4+
type InputStoreHook = [InputState, (action: InputAction) => void];
5+
6+
export const useInputStore = (): InputStoreHook => {
7+
const [, forceRender] = useReducer((s) => s + 1, 0);
8+
9+
useEffect(() => {
10+
const unsubscribe = inputStore.subscribe(forceRender);
11+
return unsubscribe; // Cleanup subscription on unmount
12+
}, []);
13+
14+
return [inputStore.getState(), inputStore.dispatch];
15+
};

0 commit comments

Comments
 (0)