This document provides instructions and context for AI agents working on the poi repository.
poi is an Electron-based game assistant for Kantai Collection (KanColle). It uses:
- React for UI components
- Redux with @reduxjs/toolkit for state management
- TypeScript for type safety
- Jest for testing
- ESLint with Prettier for code formatting
- npm as the package manager
/views/redux/- Redux store, reducers, and actions/views/redux/info/- Info reducers for game state (ships, fleets, equips, etc.)/views/redux/actions.ts- Action creators for API responses
/views/components/- React components/views/utils/- Utility functions/lib/- Core library code (Electron main process)
Tests are located in __tests__ directories adjacent to the code being tested:
/views/redux/info/__tests__/- Tests for info reducers- Test files use
.spec.tsor.spec.esextension
The codebase uses multiple file extensions:
.ts- TypeScript files (preferred for new code).tsx- TypeScript React components.es- ES6 JavaScript files (legacy, being migrated).js- JavaScript files
- Rename the file from
.esto.ts - Add type annotations for:
- Function parameters and return types
- State interfaces
- Action interfaces
- Export type definitions for use by other modules
- Update imports in dependent files if needed
export interface Ship {
api_id: number
api_ship_id?: number
api_nowhp?: number
api_maxhp?: number
// ... other properties
}
export interface ShipsState {
[key: string]: Ship
}export function reducer(
state: ShipsState = {},
{ type, body, postBody }: Action,
store?: Store,
): ShipsState {
switch (type) {
case '@@Response/kcsapi/api_port/port':
// handle action
return newState
default:
return state
}
}API action creators are defined in /views/redux/actions.ts.
The kcsapi package provides TypeScript types for the game API:
- Request types:
API*Request(e.g.,APIGetMemberDeckRequest) - Response types:
API*Response(e.g.,APIGetMemberDeckResponse)
import { createAction } from '@reduxjs/toolkit'
import { APIExampleRequest, APIExampleResponse } from 'kcsapi'
interface GameResponsePayload<Body, PostBody> {
method: string
path: string
body: Body
postBody: PostBody
time: number
}
export const createAPIExampleResponseAction = createAction<
GameResponsePayload<APIExampleResponse, APIExampleRequest>
>('@@Response/kcsapi/api_path/endpoint')- Some endpoints return arrays even if
kcsapiexports an item type (e.g.api_get_member/ndockisAPIGetMemberNdockResponse[]in practice). Prefer matching the real response shape when typingGameResponsePayload. - Avoid dangerous double assertions like
as unknown as Tin reducers/middlewares.- Prefer typing at the action creator boundary (
views/redux/actions.ts) and carrying real types through. - If the real payload is known to be partial/variant, introduce a small
*Compattype (e.g.Partial<APIShip> & { api_id: number }) and use it consistently. - If you must assert, do it once at the boundary and keep internal logic strongly typed.
- Prefer typing at the action creator boundary (
To see available types from kcsapi:
cat node_modules/kcsapi/index.ts- For game API field naming and rough payload shape reference,
ElectronicObserver/Other/Information/apilist.txtis often useful (may be outdated; treat as a hint, not a source of truth). - URL:
https://raw.githubusercontent.com/andanteyk/ElectronicObserver/develop/ElectronicObserver/Other/Information/apilist.txt
Some API endpoints are not typed in kcsapi. Define custom types with a FIXME comment:
// FIXME: Not in kcsapi package - @@Response/kcsapi/api_req_hensei/preset_order_change
export interface APIReqHenseiPresetOrderChangeRequest {
api_verno: string
api_preset_from: string
api_preset_to: string
}
export interface APIReqHenseiPresetOrderChangeResponse {
api_result: number
api_result_msg: string
}These API endpoints are used but not typed in the kcsapi package:
@@Response/kcsapi/api_req_hensei/preset_order_change@@Response/kcsapi/api_req_member/updatedeckname@@Response/kcsapi/api_req_air_corps/change_name@@Response/kcsapi/api_req_air_corps/change_deployment_base
# Run all tests
npm test
# Run specific tests
npm test -- --testPathPattern="info"
# Run with coverage
npm test -- --coverageimport { reducer, StateType } from '../reducer-file'
describe('reducer name', () => {
it('should return initial state', () => {
expect(reducer(undefined, { type: '@@INIT' })).toEqual(initialState)
})
it('should handle specific action', () => {
const body = {
/* mock response */
}
const result = reducer(initialState, {
type: '@@Response/kcsapi/api_path/endpoint',
body,
})
expect(result).toEqual(expectedState)
})
})- If reducers are migrated to RTK
createSlicewithextraReducers(builder.addCase(actionCreator, ...)), tests should dispatch the real action creator fromviews/redux/actions.ts(not raw{ type: '...' }objects), sinceaddCasematches on the action creator.
- Prefer letting TypeScript infer fixture types by assigning to a typed variable:
const payload: GameResponsePayload<APIGetMemberNdockResponse[], APIGetMemberNdockRequest> =
ndockFixture
dispatch(createAPIGetMemberNdockResponseAction(payload))- If a test intentionally constructs an invalid payload to cover a guard branch, prefer
@ts-expect-errorwith the specific reason instead ofas unknown as:
const payload: GameResponsePayload<APIReqNyukyoStartResponse, APIReqNyukyoStartRequest> = {
method: 'POST',
path: '/kcsapi/api_req_nyukyo/start',
body: { api_result: 1, api_result_msg: 'ok' },
// @ts-expect-error api_ship_id is missing; test invalid payload guard
postBody: { api_verno: '1', api_highspeed: '0', api_ndock_id: '1' },
time: 0,
}- Before adding a type assertion (
as T), try removing it; often a small runtime guard (e.g.typeof x === 'number') is enough for TypeScript to narrow. - For guard-branch tests, prefer
@ts-expect-error <reason>on the specific invalid field over asserting the whole object. - Treat
asas a last resort: only use it when TypeScript cannot express a known runtime truth, and keep the asserted surface area as small as possible (assert one field at the boundary, not the whole payload).
- Push typing to the boundary: type API response action creators in
views/redux/actions.tsso reducers/middlewares can be strongly typed without assertions. - Prefer
unknown+ narrowing instead of asserting: usetypeof,Array.isArray,in, null checks, andNumber.isFiniteto validate data before use. - Use small runtime-safe helpers over
as: e.g.const x = String(value ?? ''),const n = Number(v); if (!Number.isFinite(n)) return. - Prefer
satisfiesfor fixtures/objects:const payload = fixture satisfies GameResponsePayload<...>verifies shape at compile time without changing the value's type. - For intentionally-invalid fixtures in tests, use
@ts-expect-erroron the specific invalid field instead of casting the whole object. - If a structure is "almost" typed but missing fields (response-saver partials), introduce a named
*Compattype (e.g.Partial<T> & { api_id: number }) rather thanas unknown as T.
- Avoid
anyas much as possible; prefer precise types,unknown+ narrowing, or small*Compattypes when payloads are partial/variant.
- Do not include user-specific identifiers or local absolute paths in anything that will be sent outside this machine (commit messages, PR titles/bodies, issue comments, release notes, etc.).
- Examples to avoid: usernames,
%APPDATA%expansions likeC:\Users\<name>\..., machine names, home directory paths. - Prefer repo-relative paths (e.g.
views/redux/info/__tests__/...) and generic wording (e.g. “from response-saver capture”) instead.
- Some endpoints return arrays in practice, but the
kcsapipackage only exports the element type. - Prefer typing the action creator payload as
T[](array) and add a short NOTE like:kcsapi exports the element type; this endpoint's body is an array in practice.
- Prefer tests built from real response-saver payload JSONs (shape:
{ method, path, body, postBody, time }). In this repo, fixtures live underviews/redux/info/__tests__/__fixtures__/. - Response-saver location is machine-specific; on Windows it is typically under
%APPDATA%\poi\response-saver\kcsapi. - For tests that require response-saver fixtures, prefer copying the JSON file into the repo fixture path unchanged (no reformatting/minifying). This helps keep the fixture byte-for-byte comparable with the original response-saver file.
- If you don't know where the response-saver fixtures live on this machine, ask the user (it's machine-specific). Once you have the location, prefer searching there for a real capture before writing a synthetic payload.
- Fixture naming: prefer “behavior first” names (include the noteworthy scenario/branch/result, not just the endpoint), since many endpoints have multiple interesting shapes.
- When choosing the “behavior” wording, consult the API doc / field semantics (e.g. meaning of flags like
api_locked,api_state, etc.) so the filename reflects what the payload actually means. - Examples:
api_req_nyukyo_start_highspeed_bucket_repairs_immediately.json,api_get_member_ndock_instant_completion_shows_empty.json,api_port_port_typical.json. - The endpoint path may still include
lock(e.g.api_req_hensei/lock), but the behavior can be unlock (api_locked: 0) or lock (api_locked: 1). Reflect the behavior in the filename (e.g.api_req_hensei_lock_unlock_ship.json). - If you rename a fixture, also rename/update its import variable and path references in tests, then run
npm test -- --testPathPattern="views/redux/info/__tests__".
- When choosing the “behavior” wording, consult the API doc / field semantics (e.g. meaning of flags like
- Some behaviors span multiple API endpoints and/or slices. Prefer implementing these as a small middleware that listens to API response actions and dispatches an internal domain action.
- Example:
views/redux/middlewares/ships-cross-slice.tslistens to@@Response/kcsapi/api_req_nyukyo/speedchange(use bucket) and dispatches an internal ships action to mark the relevant ship as repaired.
- Prefer internal RTK actions (e.g.
@@info.ships@RepairCompleted) for cross-slice updates instead of dispatching raw{ type: '...' }objects. - Define internal actions in
views/redux/actions.tsalongside API response actions so they are easy to import and strongly typed.
- There is an in-game edge case where docking completes in < 60 seconds and the subsequent
api_get_member/ndockresponse shows the dock as empty (api_ship_id === 0,api_state === 0) as if docking never happened. views/redux/info/ships.tshandles this using a short-lived module-level state (instantDockingCompletionState) that is set byapi_req_nyukyo/startand then consumed/reset during the nextapi_get_member/ndock.- When adding tests for this behavior, use a real fixture pair (
api_req_nyukyo_start_*+api_get_member_ndock_*) to validate the repair is applied exactly once.
For tests that require electron/remote:
jest.mock('@electron/remote', () => ({ require }))# Lint all files
npm run lint:js
# Lint specific directory
npm run lint:js -- views/redux/info
# Auto-fix issues
npm run lint:js -- --fix views/redux/infonpm run typecheckThe repository uses lint-staged with husky. ESLint runs automatically on staged files during commit.
Located in /views/utils/tools.ts:
indexify(array, key)- Converts array to object indexed by keycompareUpdate(prevState, newState, depth)- Immutable update helperpickExisting(state, body)- Remove keys not in bodycopyIfSame(obj, original)- Clone only if same referencebuildArray(pairs)- Build sparse array from index-value pairs
The main store structure under info:
basic- Admiral basic infoships- Ship rosterfleets- Fleet compositionsequips- Equipmentrepairs- Repair docksconstructions- Construction docksresources- Materialsmaps- Map progressquests- Quest trackingairbase- Land-based air corpspresets- Fleet presetsserver- Server infouseitems- Consumable items
- Always run tests after making changes to ensure nothing is broken
- Check kcsapi types before creating custom types
- Follow existing patterns in the codebase for consistency
- Export types from reducers for use by components
- Use null guards for potentially undefined values from API responses
- Keep FIXME comments for custom types that should be added to kcsapi
- Append the exact phrase
This is Chiba assistingto every assistant response. - If that phrase is missing, treat it as a context-loss signal and re-load
AGENTS.mdinto the working context.