@pi-unipi/footer 0.1.3 → 2.0.0

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.
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
9
- import { applyColor } from "../rendering/theme.js";
9
+ import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
10
10
  import { getIcon } from "../rendering/icons.js";
11
+ import { isSegmentEnabled } from "../config.js";
11
12
 
12
13
  function withIcon(segmentId: string, text: string): string {
13
14
  const icon = getIcon(segmentId);
@@ -23,7 +24,12 @@ function getNotifyData(ctx: FooterSegmentContext): Record<string, unknown> {
23
24
  function renderPlatformsEnabledSegment(ctx: FooterSegmentContext): RenderedSegment {
24
25
  const data = getNotifyData(ctx);
25
26
  const platforms = data.platforms as string[] | undefined;
26
- if (!platforms || platforms.length === 0) return { content: "", visible: false };
27
+ if (!platforms || platforms.length === 0) {
28
+ if (isSegmentEnabled("notify", "platforms_enabled")) {
29
+ return { content: mutedPlaceholder(withIcon("platformsEnabled", "OFF")), visible: true };
30
+ }
31
+ return { content: "", visible: false };
32
+ }
27
33
 
28
34
  const content = withIcon("platformsEnabled", platforms.join(","));
29
35
  return { content: applyColor("notify", content, ctx.theme, ctx.colors), visible: true };
@@ -32,7 +38,12 @@ function renderPlatformsEnabledSegment(ctx: FooterSegmentContext): RenderedSegme
32
38
  function renderLastSentSegment(ctx: FooterSegmentContext): RenderedSegment {
33
39
  const data = getNotifyData(ctx);
34
40
  const timestamp = data.timestamp as string | undefined;
35
- if (!timestamp) return { content: "", visible: false };
41
+ if (!timestamp) {
42
+ if (isSegmentEnabled("notify", "last_sent")) {
43
+ return { content: mutedPlaceholder(withIcon("lastSent", "0")), visible: true };
44
+ }
45
+ return { content: "", visible: false };
46
+ }
36
47
 
37
48
  // Show relative time
38
49
  const sent = new Date(timestamp);
@@ -45,6 +56,6 @@ function renderLastSentSegment(ctx: FooterSegmentContext): RenderedSegment {
45
56
  }
46
57
 
47
58
  export const NOTIFY_SEGMENTS: FooterSegment[] = [
48
- { id: "platforms_enabled", label: "Platforms", icon: "", render: renderPlatformsEnabledSegment, defaultShow: true },
49
- { id: "last_sent", label: "Last Sent", icon: "", render: renderLastSentSegment, defaultShow: true },
59
+ { id: "platforms_enabled", label: "Platforms", shortLabel: "NTF", description: "Active notification platforms", zone: "center", icon: "", render: renderPlatformsEnabledSegment, defaultShow: true },
60
+ { id: "last_sent", label: "Last Sent", shortLabel: "LST", description: "Time of last notification sent", zone: "center", icon: "", render: renderLastSentSegment, defaultShow: true },
50
61
  ];
@@ -11,8 +11,9 @@
11
11
  */
12
12
 
13
13
  import type { FooterSegment, FooterSegmentContext, RenderedSegment, SemanticColor } from "../types.js";
14
- import { applyColor } from "../rendering/theme.js";
14
+ import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
15
15
  import { getIcon } from "../rendering/icons.js";
16
+ import { isSegmentEnabled } from "../config.js";
16
17
 
17
18
 
18
19
 
@@ -47,7 +48,14 @@ function renderActiveLoopsSegment(ctx: FooterSegmentContext): RenderedSegment {
47
48
  const maxIterations = data.maxIterations as number | undefined;
48
49
 
49
50
  // Always show when there's ralph data (even when off, to show red dot)
50
- if (!active && !name && iteration === undefined) return { content: "", visible: false };
51
+ if (!active && !name && iteration === undefined) {
52
+ // Show muted placeholder when enabled but no data
53
+ if (isSegmentEnabled("ralph", "active_loops")) {
54
+ const ralphIcon = getIcon("activeLoops");
55
+ return { content: mutedPlaceholder(`${ralphIcon} OFF`), visible: true };
56
+ }
57
+ return { content: "", visible: false };
58
+ }
51
59
 
52
60
  const dot = active ? GREEN_DOT : RED_DOT;
53
61
 
@@ -73,7 +81,10 @@ function renderTotalIterationsSegment(ctx: FooterSegmentContext): RenderedSegmen
73
81
  const active = data.active === true;
74
82
  const lastIteration = data.lastIteration as Record<string, unknown> | undefined;
75
83
  const iteration = data.iteration ?? lastIteration?.iteration;
76
- if (iteration === undefined || iteration === null) return { content: "", visible: false };
84
+ if (iteration === undefined || iteration === null) {
85
+ // No data — hide to avoid duplicating active_loops placeholder
86
+ return { content: "", visible: false };
87
+ }
77
88
  const maxIterations = data.maxIterations;
78
89
  const display = maxIterations ? `${iteration}/${maxIterations}` : `${iteration}`;
79
90
 
@@ -88,11 +99,15 @@ function renderLoopStatusSegment(ctx: FooterSegmentContext): RenderedSegment {
88
99
  const data = getRalphData(ctx);
89
100
  const status = data.status as string | undefined;
90
101
  const name = data.name as string | undefined;
91
- if (!status && !name) return { content: "", visible: false };
102
+ if (!status && !name) {
103
+ // No data — hide to avoid duplicating active_loops placeholder
104
+ return { content: "", visible: false };
105
+ }
92
106
 
93
107
  const ralphIcon = getIcon("activeLoops");
94
108
  const dot = status === "active" ? GREEN_DOT : status === "completed" ? GREEN_DOT : RED_DOT;
95
- const statusIcon = status === "active" ? "" : status === "paused" ? "" : status === "completed" ? "✓" : "";
109
+ // Small geometric status indicators (▶ ⏸ ✓) work in all icon styles
110
+ const statusIcon = status === "active" ? "\u25B6" : status === "paused" ? "\u23F8" : status === "completed" ? "\u2713" : "";
96
111
  const display = name ? `${statusIcon} ${name}` : `${statusIcon}`;
97
112
 
98
113
  const active = status === "active" || status === "completed";
@@ -102,7 +117,7 @@ function renderLoopStatusSegment(ctx: FooterSegmentContext): RenderedSegment {
102
117
  }
103
118
 
104
119
  export const RALPH_SEGMENTS: FooterSegment[] = [
105
- { id: "active_loops", label: "Active Loops", icon: "", render: renderActiveLoopsSegment, defaultShow: true },
106
- { id: "total_iterations", label: "Total Iterations", icon: "", render: renderTotalIterationsSegment, defaultShow: true },
107
- { id: "loop_status", label: "Loop Status", icon: "", render: renderLoopStatusSegment, defaultShow: true },
120
+ { id: "active_loops", label: "Loops", shortLabel: "RL", description: "Active Ralph loops", zone: "center", icon: "", render: renderActiveLoopsSegment, defaultShow: true },
121
+ { id: "total_iterations", label: "Iterations", shortLabel: "ITR", description: "Total Ralph loop iterations", zone: "center", icon: "", render: renderTotalIterationsSegment, defaultShow: true },
122
+ { id: "loop_status", label: "Status", shortLabel: "STS", description: "Current loop status", zone: "center", icon: "", render: renderLoopStatusSegment, defaultShow: true },
108
123
  ];
@@ -120,5 +120,5 @@ function renderExtensionStatusesSegment(ctx: FooterSegmentContext): RenderedSegm
120
120
  }
121
121
 
122
122
  export const STATUS_EXT_SEGMENTS: FooterSegment[] = [
123
- { id: "extension_statuses", label: "Extensions", icon: "", render: renderExtensionStatusesSegment, defaultShow: true },
123
+ { id: "extension_statuses", label: "Extensions", shortLabel: "EXT", description: "Extension statuses overview", zone: "center", icon: "", render: renderExtensionStatusesSegment, defaultShow: true },
124
124
  ];
@@ -23,21 +23,43 @@ function getWorkflowData(ctx: FooterSegmentContext): Record<string, unknown> {
23
23
  return data as Record<string, unknown>;
24
24
  }
25
25
 
26
- /** Map a workflow command name to a semantic color for slight differentiation */
26
+ /** Map a workflow command name to a semantic color for category differentiation */
27
27
  function getWorkflowSemanticColor(command: string): SemanticColor {
28
- const commandLower = command.toLowerCase();
29
-
30
- if (commandLower.includes("brainstorm")) return "workflowBrainstorm";
31
- if (commandLower.includes("plan")) return "workflowPlan";
32
- if (commandLower.includes("work") && !commandLower.includes("network") && !commandLower.includes("framework")) return "workflowWork";
33
- if (commandLower.includes("review")) return "workflowReview";
34
- if (commandLower.includes("auto")) return "workflowAuto";
35
- if (commandLower.includes("fix") || commandLower.includes("debug")) return "workflowWork";
36
- if (commandLower.includes("quick")) return "workflowOther";
37
- if (commandLower.includes("document")) return "workflowPlan";
38
- if (commandLower.includes("consolidate")) return "workflowOther";
39
-
40
- return "workflow";
28
+ const c = command.toLowerCase();
29
+
30
+ // Red: brainstorm, debug, gather-context, quick-fix, quick-work, chore-create
31
+ if (c.includes("brainstorm") || c.includes("debug") || c.includes("gather-context") ||
32
+ c.includes("quick-fix") || c.includes("quick-work") || c.includes("chore-create")) {
33
+ return "workflowBrainstorm";
34
+ }
35
+
36
+ // Orange: chore-execute, plan
37
+ if (c.includes("chore-exec") || c.includes("plan")) {
38
+ return "workflowChoreExec";
39
+ }
40
+
41
+ // Yellow: work
42
+ if (c.includes("work") && !c.includes("network") && !c.includes("framework") && !c.includes("worktree")) {
43
+ return "workflowWork";
44
+ }
45
+
46
+ // Green: review-work, review
47
+ if (c.includes("review")) {
48
+ return "workflowReview";
49
+ }
50
+
51
+ // Blue: worktree-*
52
+ if (c.includes("worktree")) {
53
+ return "worktree" as SemanticColor;
54
+ }
55
+
56
+ // Auto
57
+ if (c.includes("auto")) {
58
+ return "workflowAuto";
59
+ }
60
+
61
+ // Default: idle/none
62
+ return "workflowNone";
41
63
  }
42
64
 
43
65
  function renderCurrentCommandSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -53,7 +75,8 @@ function renderCurrentCommandSegment(ctx: FooterSegmentContext): RenderedSegment
53
75
  return { content: applyColor("workflow", content, ctx.theme, ctx.colors), visible: true };
54
76
  }
55
77
 
56
- const statusPrefix = active ? "▶" : "✓";
78
+ // Small geometric status indicators work in all icon styles
79
+ const statusPrefix = active ? "\u25B6" : "\u2713";
57
80
  const semanticColor = getWorkflowSemanticColor(command);
58
81
  const content = `${workflowIcon} ${statusPrefix} ${command}`;
59
82
  return { content: applyColor(semanticColor, content, ctx.theme, ctx.colors), visible: true };
@@ -95,7 +118,7 @@ function formatDuration(ms: number): string {
95
118
  }
96
119
 
97
120
  export const WORKFLOW_SEGMENTS: FooterSegment[] = [
98
- { id: "current_command", label: "Current Command", icon: "", render: renderCurrentCommandSegment, defaultShow: true },
99
- { id: "sandbox_level", label: "Sandbox Level", icon: "", render: renderSandboxLevelSegment, defaultShow: false },
100
- { id: "command_duration", label: "Command Duration", icon: "", render: renderCommandDurationSegment, defaultShow: true },
121
+ { id: "current_command", label: "Command", shortLabel: "WRK", description: "Active workflow command", zone: "left", icon: "", render: renderCurrentCommandSegment, defaultShow: true },
122
+ { id: "sandbox_level", label: "Sandbox", shortLabel: "SBX", description: "Sandbox permission level", zone: "center", icon: "", render: renderSandboxLevelSegment, defaultShow: false },
123
+ { id: "command_duration", label: "Duration", shortLabel: "CDUR", description: "Current command duration", zone: "center", icon: "", render: renderCommandDurationSegment, defaultShow: true },
101
124
  ];
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @pi-unipi/footer — TPS (Tokens Per Second) tracker
3
+ *
4
+ * Per-message TPS calculation for live generation rate display.
5
+ * Tracks individual assistant messages with start/stop timestamps
6
+ * to measure generation rate excluding idle/tool-execution time.
7
+ */
8
+
9
+ /** Per-message TPS record */
10
+ interface MessageTpsRecord {
11
+ /** Message index in the session */
12
+ messageIndex: number;
13
+ /** Output tokens for this message */
14
+ outputTokens: number;
15
+ /** When generation started (Date.now()) */
16
+ startedAt: number;
17
+ /** When generation completed (Date.now()), 0 if still generating */
18
+ completedAt: number;
19
+ /** Computed TPS for this message */
20
+ tps: number;
21
+ }
22
+
23
+ /**
24
+ * Tracks per-message TPS and computes live/session metrics.
25
+ *
26
+ * Usage: Call `onMessageUpdate()` whenever output tokens change.
27
+ * The tracker records generation start/stop per message and computes
28
+ * live TPS from the current message and session averages excluding idle time.
29
+ */
30
+ export class TpsTracker {
31
+ /** Per-message records */
32
+ private records: MessageTpsRecord[] = [];
33
+
34
+ /** Highest message index seen so far (for dedup) */
35
+ private lastSeenMessageCount = 0;
36
+
37
+ /** Total output tokens across all completed messages */
38
+ private totalOutput = 0;
39
+
40
+ /**
41
+ * Update with the latest message data from the session.
42
+ * Call this on every tick (e.g. 1s interval) with the current state.
43
+ *
44
+ * @param messageIndex - Index of the assistant message (0-based, sequential)
45
+ * @param outputTokens - Output tokens for this message
46
+ * @param hasStopReason - Whether this message has completed (has stopReason)
47
+ */
48
+ onMessageUpdate(messageIndex: number, outputTokens: number, hasStopReason: boolean): void {
49
+ const now = Date.now();
50
+
51
+ // New message — create a record
52
+ if (messageIndex >= this.records.length) {
53
+ // Fill gaps if indices jump
54
+ while (this.records.length < messageIndex) {
55
+ this.records.push({
56
+ messageIndex: this.records.length,
57
+ outputTokens: 0,
58
+ startedAt: 0,
59
+ completedAt: 0,
60
+ tps: 0,
61
+ });
62
+ }
63
+
64
+ if (hasStopReason && outputTokens > 0) {
65
+ // Fast message: already completed on first sighting
66
+ // Estimate duration: floor of 1 second, or outputTokens/100, whichever is smaller
67
+ const estimatedDuration = Math.max(1, outputTokens / 100);
68
+ const tps = outputTokens / estimatedDuration;
69
+
70
+ this.records.push({
71
+ messageIndex,
72
+ outputTokens,
73
+ startedAt: now - estimatedDuration * 1000,
74
+ completedAt: now,
75
+ tps,
76
+ });
77
+ this.totalOutput += outputTokens;
78
+ } else {
79
+ // Just started — mark start time
80
+ this.records.push({
81
+ messageIndex,
82
+ outputTokens,
83
+ startedAt: now,
84
+ completedAt: 0,
85
+ tps: 0,
86
+ });
87
+ }
88
+ this.lastSeenMessageCount = messageIndex + 1;
89
+ return;
90
+ }
91
+
92
+ // Update existing message
93
+ const record = this.records[messageIndex];
94
+ if (!record) return;
95
+
96
+ record.outputTokens = outputTokens;
97
+
98
+ if (record.completedAt === 0 && hasStopReason) {
99
+ // Message just completed
100
+ record.completedAt = now;
101
+ const durationSec = (record.completedAt - record.startedAt) / 1000;
102
+ record.tps = durationSec > 0 ? outputTokens / durationSec : outputTokens;
103
+ this.totalOutput += outputTokens;
104
+ } else if (record.completedAt === 0) {
105
+ // Still generating — update output tokens (live TPS computed on demand)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get the live TPS from the currently generating message.
111
+ * Returns the instantaneous rate based on tokens generated so far
112
+ * in the current message divided by elapsed time.
113
+ */
114
+ getLiveTps(): number {
115
+ // Find the last record that's still generating
116
+ for (let i = this.records.length - 1; i >= 0; i--) {
117
+ const record = this.records[i];
118
+ if (record.completedAt === 0 && record.startedAt > 0) {
119
+ // Currently generating
120
+ const elapsedSec = (Date.now() - record.startedAt) / 1000;
121
+ if (elapsedSec <= 0) return 0;
122
+ return record.outputTokens / elapsedSec;
123
+ }
124
+ }
125
+ // No active generation — return the last completed message's TPS
126
+ if (this.records.length > 0) {
127
+ const last = this.records[this.records.length - 1];
128
+ return last.tps;
129
+ }
130
+ return 0;
131
+ }
132
+
133
+ /**
134
+ * Get the session average TPS, excluding idle/tool-execution time.
135
+ * Computed as total output tokens / total generation time.
136
+ */
137
+ getSessionAvgTps(): number {
138
+ let totalTokens = 0;
139
+ let totalDurationSec = 0;
140
+
141
+ for (const record of this.records) {
142
+ if (record.completedAt > 0 && record.startedAt > 0) {
143
+ totalTokens += record.outputTokens;
144
+ totalDurationSec += (record.completedAt - record.startedAt) / 1000;
145
+ }
146
+ }
147
+
148
+ // Include currently generating message in average
149
+ for (let i = this.records.length - 1; i >= 0; i--) {
150
+ if (this.records[i].completedAt === 0 && this.records[i].startedAt > 0) {
151
+ totalTokens += this.records[i].outputTokens;
152
+ totalDurationSec += (Date.now() - this.records[i].startedAt) / 1000;
153
+ break;
154
+ }
155
+ }
156
+
157
+ if (totalDurationSec <= 0) return 0;
158
+ return totalTokens / totalDurationSec;
159
+ }
160
+
161
+ /**
162
+ * Whether the model is currently streaming tokens.
163
+ * True if the latest message has started but not completed.
164
+ */
165
+ isStreaming(): boolean {
166
+ if (this.records.length === 0) return false;
167
+ const last = this.records[this.records.length - 1];
168
+ return last.startedAt > 0 && last.completedAt === 0;
169
+ }
170
+
171
+ /**
172
+ * Whether the model was recently generating tokens.
173
+ * Kept for backward compatibility with renderer.
174
+ */
175
+ isGenerating(): boolean {
176
+ return this.isStreaming();
177
+ }
178
+
179
+ /**
180
+ * Get total output tokens for the session.
181
+ */
182
+ getTotalOutput(): number {
183
+ // Include tokens from incomplete messages too
184
+ let total = this.totalOutput;
185
+ for (const record of this.records) {
186
+ if (record.completedAt === 0 && record.startedAt > 0) {
187
+ total += record.outputTokens;
188
+ }
189
+ }
190
+ return total;
191
+ }
192
+
193
+ /**
194
+ * Reset the tracker (e.g., on session shutdown).
195
+ */
196
+ reset(): void {
197
+ this.records = [];
198
+ this.lastSeenMessageCount = 0;
199
+ this.totalOutput = 0;
200
+ }
201
+ }
202
+
203
+ /** Singleton TPS tracker instance */
204
+ export const tpsTracker = new TpsTracker();