ui(tui): compact 4-section monitor (was 6) — fits 1080p without zoom
Operator reported the TUI overflowed the screen unless terminal was
zoomed way out. The 11-step storyline list alone was eating ~13
rows, and each advisor/improvement entry took 2-3 rows. Now:
- Header + storyline collapsed into one panel (2 lines):
line 1: pepa · ●MC ●IPC · 1m50s · pepa_bot · (697,61,702) · HP 20 · food 5 · ☀ · ⚔60(creeper@58b)
line 2: story ▓▒░░░░░░░░░ 1/11 orient_self · Понять где я → explore.wander
The 11-step ladder is now a unicode progress bar (▓ done, ▒ current,
░ pending) — same info, fits in one row.
- Advisor entries: one line each instead of two.
✓ wedged_60s → survive.flee 802t 1900ms
(outcome mark / trigger / target skill / tokens / latency)
- Improvements entries: one line each instead of two.
#1 P2 ×3 Add craft.iron-pickaxe skill
Description dropped from the row — use `node scripts/list-improvements.js`
for full text.
- Sections: 4 (was 6).
[header+story] · [activity | chat] · [advisor | improvements] · [footer]
Tested on a typical 1080p terminal — fits comfortably without zoom.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+68
-99
@@ -140,105 +140,73 @@ function foodColor(food: number | undefined) {
|
|||||||
|
|
||||||
// --- components -------------------------------------------------------------
|
// --- components -------------------------------------------------------------
|
||||||
|
|
||||||
function Header({ snapshot, connectedIpc, width, startedAt }: { snapshot: Snapshot; connectedIpc: boolean; width: number; startedAt: number }) {
|
function StatusHeader({ snapshot, connectedIpc, width, startedAt }: { snapshot: Snapshot; connectedIpc: boolean; width: number; startedAt: number }) {
|
||||||
const pos = snapshot.position
|
const pos = snapshot.position
|
||||||
? `(${Math.round(snapshot.position.x)}, ${Math.round(snapshot.position.y)}, ${Math.round(snapshot.position.z)})`
|
? `(${Math.round(snapshot.position.x)},${Math.round(snapshot.position.y)},${Math.round(snapshot.position.z)})`
|
||||||
: "?";
|
: "?";
|
||||||
const hp = snapshot.health;
|
const hp = snapshot.health;
|
||||||
const food = snapshot.food;
|
const food = snapshot.food;
|
||||||
const day = snapshot.isDay ? "☀ day" : "🌙 night";
|
const day = snapshot.isDay ? "☀" : "🌙";
|
||||||
const session = formatDuration(Date.now() - startedAt);
|
const session = formatDuration(Date.now() - startedAt);
|
||||||
const mcOnline = snapshot.connected;
|
const mcOnline = snapshot.connected;
|
||||||
|
const story = snapshot.storyStep;
|
||||||
|
const idx = story?.index ?? 0;
|
||||||
|
const cur = story?.step;
|
||||||
|
const want = story?.suggestion?.skillId;
|
||||||
|
// 11-step progress bar in 11 cells
|
||||||
|
const bar = Array.from({ length: 11 }, (_, i) => {
|
||||||
|
if (i < idx) return "▓";
|
||||||
|
if (i === idx) return "▒";
|
||||||
|
return "░";
|
||||||
|
}).join("");
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} width={width}>
|
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} width={width}>
|
||||||
<Box>
|
<Box>
|
||||||
<Text bold color="cyan">pepa-monitor</Text>
|
<Text bold color="cyan">pepa</Text>
|
||||||
<Text dimColor> · </Text>
|
<Text dimColor> · </Text>
|
||||||
<Text color={mcOnline ? "green" : "red"}>{mcOnline ? "● MC" : "○ MC"}</Text>
|
<Text color={mcOnline ? "green" : "red"}>{mcOnline ? "●MC" : "○MC"}</Text>
|
||||||
<Text dimColor> · </Text>
|
<Text dimColor> · </Text>
|
||||||
<Text color={connectedIpc ? "green" : "red"}>{connectedIpc ? "● IPC" : "○ IPC"}</Text>
|
<Text color={connectedIpc ? "green" : "red"}>{connectedIpc ? "●IPC" : "○IPC"}</Text>
|
||||||
<Text dimColor> · </Text>
|
<Text dimColor> · </Text>
|
||||||
<Text>session {session}</Text>
|
<Text>{session}</Text>
|
||||||
|
<Text dimColor> · </Text>
|
||||||
|
<Text bold>{snapshot.username ?? "?"}</Text>
|
||||||
|
<Text dimColor> · </Text>
|
||||||
|
<Text>{pos}</Text>
|
||||||
|
<Text dimColor> · </Text>
|
||||||
|
<Text>HP </Text><Text bold color={hpColor(hp)}>{hp ?? "?"}</Text>
|
||||||
|
<Text dimColor> · </Text>
|
||||||
|
<Text>food </Text><Text bold color={foodColor(food)}>{food ?? "?"}</Text>
|
||||||
<Text dimColor> · </Text>
|
<Text dimColor> · </Text>
|
||||||
<Text>user </Text><Text bold>{snapshot.username ?? "?"}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text>pos </Text><Text bold>{pos}</Text>
|
|
||||||
<Text dimColor> </Text>
|
|
||||||
<Text>HP </Text>
|
|
||||||
<Text bold color={hpColor(hp)}>{hp ?? "?"}/20</Text>
|
|
||||||
<Text dimColor> </Text>
|
|
||||||
<Text>food </Text>
|
|
||||||
<Text bold color={foodColor(food)}>{food ?? "?"}/20</Text>
|
|
||||||
<Text dimColor> </Text>
|
|
||||||
<Text>{day}</Text>
|
<Text>{day}</Text>
|
||||||
<Text dimColor> hostiles </Text>
|
{(snapshot.hostileCount ?? 0) > 0 ? (
|
||||||
<Text color={(snapshot.hostileCount ?? 0) > 0 ? "red" : "gray"}>{snapshot.hostileCount ?? 0}</Text>
|
<>
|
||||||
{snapshot.closestHostile ? <Text color="red"> closest {snapshot.closestHostile.name}@{snapshot.closestHostile.distance}b</Text> : null}
|
<Text dimColor> · </Text>
|
||||||
|
<Text color="red">⚔{snapshot.hostileCount}</Text>
|
||||||
|
{snapshot.closestHostile ? <Text color="red">({snapshot.closestHostile.name}@{Math.round(snapshot.closestHostile.distance)}b)</Text> : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StorylinePanel({ snapshot, width }: { snapshot: Snapshot; width: number }) {
|
|
||||||
const story = snapshot.storyStep;
|
|
||||||
if (!story) {
|
|
||||||
return (
|
|
||||||
<Box borderStyle="round" borderColor="gray" paddingX={1} width={width}>
|
|
||||||
<Text dimColor>storyline not available (bot offline or storyline disabled)</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const STEPS = [
|
|
||||||
"orient_self", "first_wood", "crafting_basics", "first_tools", "first_food",
|
|
||||||
"shelter_minimal", "stone_tier", "food_security", "iron_age", "settle_base", "village_grow",
|
|
||||||
];
|
|
||||||
const TITLES: Record<string, string> = {
|
|
||||||
orient_self: "Понять где я",
|
|
||||||
first_wood: "Собрать 8 поленьев",
|
|
||||||
crafting_basics: "Сделать верстак и палки",
|
|
||||||
first_tools: "Деревянные орудия",
|
|
||||||
first_food: "Найти первую еду",
|
|
||||||
shelter_minimal: "Простой шелтер с кроватью",
|
|
||||||
stone_tier: "Каменные орудия",
|
|
||||||
food_security: "Запас еды на 16+",
|
|
||||||
iron_age: "Железо и печь",
|
|
||||||
settle_base: "Постоянная база",
|
|
||||||
village_grow: "Развивать деревню",
|
|
||||||
};
|
|
||||||
const idx = story.index ?? 0;
|
|
||||||
const cur = story.step;
|
|
||||||
const want = story.suggestion?.skillId;
|
|
||||||
const emergency = story.emergency;
|
|
||||||
return (
|
|
||||||
<Box flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={1} width={width}>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text bold color="magenta">storyline</Text>
|
<Text color="magenta">story </Text>
|
||||||
<Text dimColor> step </Text>
|
<Text bold>{bar}</Text>
|
||||||
|
<Text dimColor> </Text>
|
||||||
<Text bold>{idx + 1}/11</Text>
|
<Text bold>{idx + 1}/11</Text>
|
||||||
<Text dimColor> </Text>
|
<Text dimColor> </Text>
|
||||||
<Text bold color="white">{cur?.id}</Text>
|
{cur ? (
|
||||||
<Text dimColor> {cur?.title}</Text>
|
<>
|
||||||
{emergency ? <Text color="red" bold> [EMERGENCY PAUSE]</Text> : null}
|
<Text color="white" bold>{cur.id}</Text>
|
||||||
</Box>
|
<Text dimColor> · </Text>
|
||||||
|
<Text>{cur.title}</Text>
|
||||||
|
</>
|
||||||
|
) : <Text dimColor>(no story)</Text>}
|
||||||
{want ? (
|
{want ? (
|
||||||
<Box>
|
<>
|
||||||
<Text dimColor>wants: </Text>
|
<Text dimColor> → </Text>
|
||||||
<Text color="cyan">{want}</Text>
|
<Text color="cyan">{want}</Text>
|
||||||
</Box>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<Box flexDirection="column" marginTop={1}>
|
{story?.emergency ? <Text color="red" bold> [EMERGENCY]</Text> : null}
|
||||||
{STEPS.map((id, i) => {
|
|
||||||
const done = i < idx;
|
|
||||||
const current = i === idx;
|
|
||||||
const mark = done ? "✓" : current ? "→" : "○";
|
|
||||||
const color = done ? "green" : current ? "yellow" : "gray";
|
|
||||||
return (
|
|
||||||
<Text key={id} color={color}>
|
|
||||||
{` ${mark} ${(i + 1).toString().padStart(2)}. ${id.padEnd(18)} ${TITLES[id] ?? ""}`}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -300,17 +268,20 @@ function AdvisorPanel({ recs, width, height }: { recs: any[]; width: number; hei
|
|||||||
const ok = r.outcome_ok;
|
const ok = r.outcome_ok;
|
||||||
const outcomeMark = ok == null ? "·" : ok ? "✓" : "✗";
|
const outcomeMark = ok == null ? "·" : ok ? "✓" : "✗";
|
||||||
const outcomeColor = ok == null ? "gray" : ok ? "green" : "red";
|
const outcomeColor = ok == null ? "gray" : ok ? "green" : "red";
|
||||||
const tail = `${r.tokens_in ?? "?"}/${r.tokens_out ?? "?"}t ${r.latency_ms ?? "?"}ms`;
|
const target = r.recommended_skill ?? r.action;
|
||||||
|
const trigger = String(r.trigger_reason ?? "?");
|
||||||
|
// One line per recommendation: " ✓ wedged_60s → survive.flee 802t 1900ms"
|
||||||
|
const lhs = `${outcomeMark} ${trigger} → ${target}`;
|
||||||
|
const rhs = `${r.tokens_in ?? "?"}t ${r.latency_ms ?? "?"}ms`;
|
||||||
|
const free = Math.max(20, width - rhs.length - 6);
|
||||||
return (
|
return (
|
||||||
<Box key={`adv-${r.id}`} flexDirection="column">
|
<Text key={`adv-${r.id}`}>
|
||||||
<Text>
|
|
||||||
<Text color={outcomeColor} bold>{outcomeMark} </Text>
|
<Text color={outcomeColor} bold>{outcomeMark} </Text>
|
||||||
<Text color="white">{r.trigger_reason}</Text>
|
<Text color="white">{trigger}</Text>
|
||||||
<Text dimColor> → </Text>
|
<Text dimColor> → </Text>
|
||||||
<Text color="cyan">{r.recommended_skill ?? r.action}</Text>
|
<Text color="cyan">{String(target).slice(0, Math.max(8, free - trigger.length - 6))}</Text>
|
||||||
|
<Text dimColor> {rhs}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<Text dimColor> {tail}{r.rationale ? ` · ${String(r.rationale).slice(0, width - 14)}` : ""}</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{visible.length === 0 ? <Text dimColor>(no advisor calls yet)</Text> : null}
|
{visible.length === 0 ? <Text dimColor>(no advisor calls yet)</Text> : null}
|
||||||
@@ -322,19 +293,14 @@ function ImprovementsPanel({ items, width, height }: { items: any[]; width: numb
|
|||||||
const visible = items.slice(0, height);
|
const visible = items.slice(0, height);
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={1} width={width}>
|
<Box flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={1} width={width}>
|
||||||
<Text bold color="magenta">Improvements (open: {items.length})</Text>
|
<Text bold color="magenta">Improvements (open {items.length})</Text>
|
||||||
{visible.map((r) => (
|
{visible.map((r) => (
|
||||||
<Box key={`imp-${r.id}`} flexDirection="column">
|
<Text key={`imp-${r.id}`}>
|
||||||
<Text>
|
|
||||||
<Text color="gray">#{r.id} </Text>
|
<Text color="gray">#{r.id} </Text>
|
||||||
<Text color="yellow">P{r.priority}</Text>
|
<Text color="yellow">P{r.priority}</Text>
|
||||||
<Text dimColor> ×{r.votes} </Text>
|
<Text dimColor> ×{r.votes} </Text>
|
||||||
<Text color="white">{String(r.title).slice(0, width - 14)}</Text>
|
<Text color="white">{String(r.title).slice(0, Math.max(20, width - 14))}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
{r.description ? (
|
|
||||||
<Text dimColor> {String(r.description).slice(0, width - 8)}</Text>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
))}
|
))}
|
||||||
{visible.length === 0 ? <Text dimColor>(no improvement requests yet)</Text> : null}
|
{visible.length === 0 ? <Text dimColor>(no improvement requests yet)</Text> : null}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -477,17 +443,20 @@ function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout math: full-width header & storyline, then split 50/50.
|
// Layout math: compact 4-section vertical stack.
|
||||||
|
// row budget:
|
||||||
|
// header (story + status) ≈ 4 rows
|
||||||
|
// middle activity/chat ≈ floor((rows - 4 - 4 - 4) / 2)
|
||||||
|
// bottom advisor/improvements ≈ same
|
||||||
|
// footer ≈ 4 rows
|
||||||
const totalWidth = Math.max(80, cols);
|
const totalWidth = Math.max(80, cols);
|
||||||
const halfWidth = Math.floor(totalWidth / 2);
|
const halfWidth = Math.floor(totalWidth / 2);
|
||||||
const totalRows = Math.max(24, rows);
|
const totalRows = Math.max(20, rows);
|
||||||
// rough allocation: header 4, storyline ~14, footer 4. Rest split for middle panels.
|
const middleRows = Math.max(5, Math.floor((totalRows - 4 - 4 - 4) / 2));
|
||||||
const middleRows = Math.max(8, Math.floor((totalRows - 4 - 14 - 4) / 2));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" width={totalWidth}>
|
<Box flexDirection="column" width={totalWidth}>
|
||||||
<Header snapshot={state.snapshot} connectedIpc={state.connectedIpc} width={totalWidth} startedAt={state.startedAt} />
|
<StatusHeader snapshot={state.snapshot} connectedIpc={state.connectedIpc} width={totalWidth} startedAt={state.startedAt} />
|
||||||
<StorylinePanel snapshot={state.snapshot} width={totalWidth} />
|
|
||||||
<Box flexDirection="row" width={totalWidth}>
|
<Box flexDirection="row" width={totalWidth}>
|
||||||
<ActivityPanel dispatches={state.dispatches} width={halfWidth} height={middleRows} />
|
<ActivityPanel dispatches={state.dispatches} width={halfWidth} height={middleRows} />
|
||||||
<ChatPanel chat={state.chat} width={totalWidth - halfWidth} height={middleRows} />
|
<ChatPanel chat={state.chat} width={totalWidth - halfWidth} height={middleRows} />
|
||||||
|
|||||||
Reference in New Issue
Block a user