opencode-top 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-top",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Monitor OpenCode AI coding sessions - Token usage, costs, and agent analytics",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ui/App.tsx CHANGED
@@ -29,6 +29,19 @@ export function App({ refreshInterval = 2000 }: AppProps) {
29
29
  const terminalHeight = stdout?.rows ?? 24;
30
30
  const terminalWidth = stdout?.columns ?? 80;
31
31
 
32
+ // Enter alternate screen buffer on mount — prevents Ink's clearTerminal flash.
33
+ // Ink triggers \x1b[2J\x1b[3J\x1b[H whenever outputHeight >= stdout.rows.
34
+ // Alt screen (\x1b[?1049h) moves rendering to a separate buffer so the main
35
+ // screen is never touched, eliminating the black flash entirely.
36
+ useEffect(() => {
37
+ stdout?.write("\x1b[?1049h"); // enter alt screen
38
+ stdout?.write("\x1b[?25l"); // hide cursor
39
+ return () => {
40
+ stdout?.write("\x1b[?25h"); // restore cursor
41
+ stdout?.write("\x1b[?1049l"); // leave alt screen
42
+ };
43
+ }, [stdout]);
44
+
32
45
  const [workflows, setWorkflows] = useState<Workflow[]>([]);
33
46
  const [screen, setScreen] = useState<ScreenId>("sessions");
34
47
  const [error, setError] = useState<string | null>(null);
@@ -108,7 +121,7 @@ export function App({ refreshInterval = 2000 }: AppProps) {
108
121
  const contentHeight = terminalHeight - 3;
109
122
 
110
123
  return (
111
- <Box flexDirection="column" width={terminalWidth} height={terminalHeight}>
124
+ <Box flexDirection="column" width={terminalWidth} height={terminalHeight - 1}>
112
125
  <TabBar activeScreen={screen} lastRefresh={lastRefresh} />
113
126
  <Box width={terminalWidth} height={contentHeight}>
114
127
  {screen === "sessions" && (
@@ -10,11 +10,11 @@ interface MessagesPanelProps {
10
10
  }
11
11
 
12
12
  type MsgLine =
13
- | { kind: "header"; modelId: string; agent: string | null; duration: string; time: string }
14
- | { kind: "tool"; callId: string; icon: string; iconColor: string; name: string; title: string; right: string; expanded: boolean }
15
- | { kind: "tool-detail"; label: string; value: string; isSection: boolean }
16
- | { kind: "text"; text: string }
17
- | { kind: "reasoning"; text: string };
13
+ | { id: string; kind: "header"; modelId: string; agent: string | null; duration: string; time: string }
14
+ | { id: string; kind: "tool"; callId: string; icon: string; iconColor: string; name: string; title: string; right: string; expanded: boolean }
15
+ | { id: string; kind: "tool-detail"; label: string; value: string; isSection: boolean }
16
+ | { id: string; kind: "text"; text: string }
17
+ | { id: string; kind: "reasoning"; text: string };
18
18
 
19
19
  function buildLines(session: Session, contentWidth: number, expandedIds: Set<string>): MsgLine[] {
20
20
  const lines: MsgLine[] = [];
@@ -27,6 +27,7 @@ function buildLines(session: Session, contentWidth: number, expandedIds: Set<str
27
27
  : "";
28
28
 
29
29
  lines.push({
30
+ id: `h-${interaction.id}`,
30
31
  kind: "header",
31
32
  modelId: interaction.modelId,
32
33
  agent: interaction.agent ?? null,
@@ -45,6 +46,7 @@ function buildLines(session: Session, contentWidth: number, expandedIds: Set<str
45
46
  const expanded = expandedIds.has(p.callId);
46
47
 
47
48
  lines.push({
49
+ id: `t-${p.callId}`,
48
50
  kind: "tool",
49
51
  callId: p.callId,
50
52
  icon,
@@ -58,29 +60,32 @@ function buildLines(session: Session, contentWidth: number, expandedIds: Set<str
58
60
  if (expanded) {
59
61
  const inputKeys = Object.keys(p.input);
60
62
  if (inputKeys.length > 0) {
61
- lines.push({ kind: "tool-detail", label: "input", value: "", isSection: true });
63
+ lines.push({ id: `td-${p.callId}-in`, kind: "tool-detail", label: "input", value: "", isSection: true });
62
64
  for (const key of inputKeys) {
63
65
  const val = formatParamValue(p.input[key], contentWidth - key.length - 6);
64
- lines.push({ kind: "tool-detail", label: key, value: val, isSection: false });
66
+ lines.push({ id: `td-${p.callId}-in-${key}`, kind: "tool-detail", label: key, value: val, isSection: false });
65
67
  }
66
68
  }
67
69
  if (p.output?.trim()) {
68
- lines.push({ kind: "tool-detail", label: "output", value: "", isSection: true });
70
+ lines.push({ id: `td-${p.callId}-out`, kind: "tool-detail", label: "output", value: "", isSection: true });
69
71
  const outLines = p.output.trim().split("\n").slice(0, 40);
72
+ let outIdx = 0;
70
73
  for (const ol of outLines) {
71
74
  for (const wrapped of wrapText(ol === "" ? " " : ol, contentWidth - 5)) {
72
- lines.push({ kind: "tool-detail", label: "", value: wrapped, isSection: false });
75
+ lines.push({ id: `td-${p.callId}-out-${outIdx++}`, kind: "tool-detail", label: "", value: wrapped, isSection: false });
73
76
  }
74
77
  }
75
78
  }
76
79
  }
77
80
  } else if (part.type === "text" && part.text.trim()) {
81
+ let txtIdx = 0;
78
82
  for (const row of wrapText(part.text.trim(), contentWidth - 3)) {
79
- lines.push({ kind: "text", text: row });
83
+ lines.push({ id: `tx-${interaction.id}-${txtIdx++}`, kind: "text", text: row });
80
84
  }
81
85
  } else if (part.type === "reasoning" && part.text.trim()) {
86
+ let rIdx = 0;
82
87
  for (const row of wrapText(part.text.trim(), contentWidth - 5)) {
83
- lines.push({ kind: "reasoning", text: row });
88
+ lines.push({ id: `r-${interaction.id}-${rIdx++}`, kind: "reasoning", text: row });
84
89
  }
85
90
  }
86
91
  }
@@ -251,7 +256,7 @@ function MessagesPanelInner({ session, maxHeight, isActive }: MessagesPanelProps
251
256
  switch (line.kind) {
252
257
  case "header":
253
258
  return (
254
- <Box key={absIdx} flexDirection="row" height={1}>
259
+ <Box key={line.id} flexDirection="row" height={1}>
255
260
  <Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
256
261
  <Text color={isCursor ? colors.accent : colors.purple} bold>◆ </Text>
257
262
  <Text color={isCursor ? colors.accent : colors.info}>{truncate(line.modelId, 26)}</Text>
@@ -264,7 +269,7 @@ function MessagesPanelInner({ session, maxHeight, isActive }: MessagesPanelProps
264
269
 
265
270
  case "tool":
266
271
  return (
267
- <Box key={absIdx} flexDirection="row" height={1} paddingLeft={1}>
272
+ <Box key={line.id} flexDirection="row" height={1} paddingLeft={1}>
268
273
  <Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
269
274
  <Text color={line.iconColor}>{line.icon} </Text>
270
275
  <Text color={isCursor ? colors.accent : colors.text} bold={isCursor}>{line.name}</Text>
@@ -278,14 +283,14 @@ function MessagesPanelInner({ session, maxHeight, isActive }: MessagesPanelProps
278
283
  case "tool-detail":
279
284
  if (line.isSection) {
280
285
  return (
281
- <Box key={absIdx} height={1} paddingLeft={3}>
286
+ <Box key={line.id} height={1} paddingLeft={3}>
282
287
  <Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
283
288
  <Text color={colors.purple}>── {line.label} </Text>
284
289
  </Box>
285
290
  );
286
291
  }
287
292
  return (
288
- <Box key={absIdx} height={1} paddingLeft={4}>
293
+ <Box key={line.id} height={1} paddingLeft={4}>
289
294
  <Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
290
295
  {line.label
291
296
  ? <><Text color={colors.cyan}>{line.label}</Text><Text color={colors.textDim}>: </Text><Text color={isCursor ? colors.accent : colors.text}>{line.value}</Text></>
@@ -296,7 +301,7 @@ function MessagesPanelInner({ session, maxHeight, isActive }: MessagesPanelProps
296
301
 
297
302
  case "text":
298
303
  return (
299
- <Box key={absIdx} flexDirection="row" height={1} paddingLeft={1}>
304
+ <Box key={line.id} flexDirection="row" height={1} paddingLeft={1}>
300
305
  <Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
301
306
  <Text color={isCursor ? colors.accent : colors.text}> {line.text}</Text>
302
307
  </Box>
@@ -304,7 +309,7 @@ function MessagesPanelInner({ session, maxHeight, isActive }: MessagesPanelProps
304
309
 
305
310
  case "reasoning":
306
311
  return (
307
- <Box key={absIdx} flexDirection="row" height={1} paddingLeft={1}>
312
+ <Box key={line.id} flexDirection="row" height={1} paddingLeft={1}>
308
313
  <Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
309
314
  <Text color={isCursor ? colors.accent : colors.accentDim}> ⚡ {line.text}</Text>
310
315
  </Box>