Files
handler/app/src/state/AppState.tsx
T
Claude daecaa7e6b Implement Handler mobile app (React Native + Expo iOS)
Build the committed 2a interactive prototype from Handler Mobile.dc.html as
a real Expo app: six wired screens (Fleet, Agent detail, Answer, Spawn, Log,
Settings) with the exact state logic ported from the design's renderVals().

- Port Leeworks tokens (colors light/dark, typography, spacing, radii,
  shadows) to typed RN values in src/theme.
- Reimplement the design-system components used by the screens (Button,
  Badge, Icon, Switch, Select, TextField, segmented control, chip, tab bar).
- Shared store mirrors the prototype's single-screen navigation and the
  Waiting -> Running flip when agt-7a1d is answered.
- Real OS status bar / home indicator (safe-area insets) replace the mock
  phone chrome; dark mode follows system appearance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdF2hHVQ4UUBFPtpeMQ81o
2026-07-20 18:48:30 +00:00

128 lines
3.6 KiB
TypeScript

import React, { createContext, useContext, useMemo, useState } from "react";
/**
* Central prototype state, ported 1:1 from the DC script's
* `state` + `renderVals()` in project/Handler Mobile.dc.html (turn 2a).
*
* The prototype navigates by swapping a single `screen` value rather than a
* stack, and answering agt-7a1d flips it Waiting -> Running everywhere. That
* cross-screen behavior is exactly why this lives in one shared store.
*/
export type Screen =
| "fleet"
| "detail"
| "answer"
| "spawn"
| "log"
| "settings";
export type DetailTab = "state" | "log";
export type LogFilter = "all" | "handler" | "errors";
export type BadgeTone = "neutral" | "positive" | "warning" | "danger";
const ANSWERED_STATE =
"Answer received: keep the JSON store as a read-only fallback for one release. Deprecating writes now; removal ticket filed for next cycle.";
const WAITING_STATE =
"Schema written, migrations pass. Blocked on the legacy JSON store — drop it or keep as read fallback? Holding before deleting store/json.rs.";
interface AppStateValue {
screen: Screen;
detailTab: DetailTab;
quickPick: number | null;
logFilter: LogFilter;
answered: boolean;
swEdits: boolean;
swTests: boolean;
swPushWait: boolean;
swPushFail: boolean;
// Derived (mirrors renderVals()).
notAnswered: boolean;
agentTone: BadgeTone;
agentStatus: string;
agentStateText: string;
go: (screen: Screen) => void;
setDetailTab: (tab: DetailTab) => void;
setQuickPick: (i: number) => void;
setLogFilter: (f: LogFilter) => void;
sendResume: () => void;
spawnGo: () => void;
setSwEdits: (v: boolean) => void;
setSwTests: (v: boolean) => void;
setSwPushWait: (v: boolean) => void;
setSwPushFail: (v: boolean) => void;
}
const AppStateContext = createContext<AppStateValue | null>(null);
export function AppStateProvider({ children }: { children: React.ReactNode }) {
const [screen, setScreen] = useState<Screen>("fleet");
const [detailTab, setDetailTab] = useState<DetailTab>("state");
const [quickPick, setQuickPickState] = useState<number | null>(null);
const [logFilter, setLogFilter] = useState<LogFilter>("all");
const [answered, setAnswered] = useState(false);
const [swEdits, setSwEdits] = useState(false);
const [swTests, setSwTests] = useState(true);
const [swPushWait, setSwPushWait] = useState(true);
const [swPushFail, setSwPushFail] = useState(true);
const value = useMemo<AppStateValue>(
() => ({
screen,
detailTab,
quickPick,
logFilter,
answered,
swEdits,
swTests,
swPushWait,
swPushFail,
notAnswered: !answered,
agentTone: answered ? "positive" : "warning",
agentStatus: answered ? "Running" : "Waiting",
agentStateText: answered ? ANSWERED_STATE : WAITING_STATE,
go: setScreen,
setDetailTab,
setQuickPick: setQuickPickState,
setLogFilter,
sendResume: () => {
setAnswered(true);
setDetailTab("state");
setScreen("detail");
},
spawnGo: () => setScreen("fleet"),
setSwEdits,
setSwTests,
setSwPushWait,
setSwPushFail,
}),
[
screen,
detailTab,
quickPick,
logFilter,
answered,
swEdits,
swTests,
swPushWait,
swPushFail,
]
);
return (
<AppStateContext.Provider value={value}>
{children}
</AppStateContext.Provider>
);
}
export function useAppState(): AppStateValue {
const ctx = useContext(AppStateContext);
if (!ctx) throw new Error("useAppState must be used within AppStateProvider");
return ctx;
}