@selesai/code 0.1.19 → 0.2.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.
Files changed (156) hide show
  1. package/dist/agents/architect.md +4 -3
  2. package/dist/agents/builder.md +5 -4
  3. package/dist/agents/commentator.md +3 -2
  4. package/dist/agents/explorer.md +4 -4
  5. package/dist/agents/recapper.md +4 -3
  6. package/dist/config.d.ts +6 -7
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +18 -24
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts.map +1 -1
  17. package/dist/core/extensions/loader.js +2 -0
  18. package/dist/core/extensions/loader.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +8 -1
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/skills.d.ts.map +1 -1
  23. package/dist/core/skills.js +25 -11
  24. package/dist/core/skills.js.map +1 -1
  25. package/dist/defaults/settings.json +2 -29
  26. package/dist/extensions/package.json +5 -4
  27. package/dist/extensions/pi-intercom/index.ts +12 -5
  28. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  29. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  30. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  31. package/dist/extensions/pi-subagents/README.md +12 -5
  32. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  33. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  34. package/dist/extensions/pi-subagents/package.json +8 -5
  35. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  36. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  37. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  38. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  39. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  40. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  41. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  42. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  43. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  44. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  45. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  46. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  48. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  49. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  51. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  52. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  53. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  56. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  57. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  59. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  61. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  62. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  63. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  64. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  65. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  66. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  67. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  68. package/dist/extensions/pi-web-agent/package.json +31 -0
  69. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  70. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  71. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  72. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  73. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  74. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  75. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  76. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  77. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  78. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  79. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  80. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  81. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  82. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  91. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  92. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  93. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  94. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  98. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  99. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  100. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  101. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  102. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  103. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  104. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  105. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  106. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  107. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  108. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  109. package/dist/extensions/ponytail/index.js +189 -0
  110. package/dist/extensions/ponytail/package.json +8 -0
  111. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  112. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  113. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  114. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  115. package/dist/extensions/question/constants.ts +38 -0
  116. package/dist/extensions/question/dialog-adapter.ts +74 -0
  117. package/dist/extensions/question/helpers.ts +102 -0
  118. package/dist/extensions/question/index.ts +671 -0
  119. package/dist/extensions/question/navigation.ts +14 -0
  120. package/dist/extensions/question/package.json +18 -0
  121. package/dist/extensions/question/question-list.ts +498 -0
  122. package/dist/extensions/question/row-layout.ts +162 -0
  123. package/dist/extensions/question/schemas.ts +69 -0
  124. package/dist/extensions/question/selection-mode.ts +49 -0
  125. package/dist/extensions/question/shortcuts.ts +53 -0
  126. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  127. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  128. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  129. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  130. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  131. package/dist/extensions/question/tui-adapter.ts +33 -0
  132. package/dist/extensions/question/types.ts +102 -0
  133. package/dist/extensions/question/ui-protocol.ts +46 -0
  134. package/dist/extensions/tokenin-onboarding.ts +265 -2
  135. package/dist/extensions/web-agent-onboarding.ts +222 -0
  136. package/dist/extensions/workflow/adapter.ts +588 -0
  137. package/dist/extensions/workflow/extension.ts +24 -0
  138. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  139. package/dist/extensions/workflow/modes/quick.ts +138 -0
  140. package/dist/extensions/workflow/package.json +17 -0
  141. package/dist/extensions/workflow/state-machine.ts +386 -0
  142. package/dist/index.d.ts +1 -1
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +1 -1
  145. package/dist/index.js.map +1 -1
  146. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  147. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  148. package/dist/modes/interactive/interactive-mode.js +57 -13
  149. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  150. package/dist/utils/mime.d.ts.map +1 -1
  151. package/dist/utils/mime.js +24 -1
  152. package/dist/utils/mime.js.map +1 -1
  153. package/docs/docs.json +4 -0
  154. package/docs/workflows.md +206 -0
  155. package/package.json +5 -1
  156. package/README.md +0 -198
@@ -20,27 +20,8 @@
20
20
  "packages": [
21
21
  "git:github.com/MasuRii/pi-tool-display",
22
22
  "git:github.com/omaclaren/pi-markdown-preview",
23
- "git:github.com/aliou/pi-guardrails",
24
- "git:github.com/nicobailon/pi-intercom",
25
- "git:github.com/nicobailon/pi-rewind-hook"
23
+ "git:github.com/aliou/pi-guardrails"
26
24
  ],
27
- "powerline": {
28
- "preset": "custom",
29
- "fixedEditor": true,
30
- "fixedEditorPromptGlyph": "",
31
- "mouseScroll": true,
32
- "customItems": [
33
- {
34
- "id": "tps",
35
- "statusKey": "tps",
36
- "position": "left",
37
- "prefix": "",
38
- "color": "success",
39
- "hideWhenMissing": true,
40
- "excludeFromExtensionStatuses": true
41
- }
42
- ]
43
- },
44
25
  "quietStartup": true,
45
26
  "retry": {
46
27
  "enabled": true,
@@ -53,13 +34,5 @@
53
34
  },
54
35
  "theme": "dark",
55
36
  "transport": "websocket",
56
- "treeFilterMode": "no-tools",
57
- "webSearch": {
58
- "apiKeys": {
59
- "BRAVE_API_KEY": "",
60
- "EXA_API_KEY": "",
61
- "JINA_API_KEY": "",
62
- "TAVILY_API_KEY": ""
63
- }
64
- }
37
+ "treeFilterMode": "no-tools"
65
38
  }
@@ -7,14 +7,15 @@
7
7
  "extensions": [
8
8
  "./copy-turn.ts",
9
9
  "./gitignore-guard.ts",
10
- "./question.ts",
10
+ "./tool-error-autofix.ts",
11
+ "./question",
11
12
  "./rtk.ts",
12
- "./tps-tracker.ts",
13
13
  "./tokenin-onboarding.ts",
14
14
  "./undo.ts",
15
- "./prototype.ts",
16
- "./quick.ts",
15
+ "./workflow",
17
16
  "./pi-subagents",
17
+ "./pi-web-agent",
18
+ "./web-agent-onboarding.ts",
18
19
  "./pi-powerline-footer",
19
20
  "./pi-extension"
20
21
  ]
@@ -17,11 +17,11 @@ const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delive
17
17
  const INBOUND_FLUSH_DELAY_MS = 200;
18
18
  const INBOUND_IDLE_RETRY_MS = 500;
19
19
  const DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX = "subagent-chat";
20
- const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
21
- const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
22
- const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
23
- const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
24
- const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
20
+ const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "SELESAI_SUBAGENT_ORCHESTRATOR_TARGET";
21
+ const SUBAGENT_RUN_ID_ENV = "SELESAI_SUBAGENT_RUN_ID";
22
+ const SUBAGENT_CHILD_AGENT_ENV = "SELESAI_SUBAGENT_CHILD_AGENT";
23
+ const SUBAGENT_CHILD_INDEX_ENV = "SELESAI_SUBAGENT_CHILD_INDEX";
24
+ const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "SELESAI_SUBAGENT_INTERCOM_SESSION_NAME";
25
25
 
26
26
  interface ChildOrchestratorMetadata {
27
27
  orchestratorTarget: string;
@@ -1006,6 +1006,13 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1006
1006
  syncPresenceIdentity(ctx.sessionManager.getSessionId());
1007
1007
  replyTracker.beginTurn();
1008
1008
  });
1009
+ pi.on("session_info_changed", (_event, ctx) => {
1010
+ if (!getLiveContext(ctx)) {
1011
+ return;
1012
+ }
1013
+ currentSessionId = ctx.sessionManager.getSessionId();
1014
+ syncPresenceIdentity(ctx.sessionManager.getSessionId());
1015
+ });
1009
1016
  pi.on("model_select", (event, ctx) => {
1010
1017
  if (!getLiveContext(ctx)) {
1011
1018
  return;
@@ -43,7 +43,7 @@ import {
43
43
  shortcutConflictKey,
44
44
  shortcutUsesSuper,
45
45
  } from "./shortcuts.ts";
46
- import {
46
+ import {
47
47
  initVibeManager,
48
48
  onVibeBeforeAgentStart,
49
49
  onVibeAgentStart,
@@ -59,6 +59,7 @@ import {
59
59
  getVibeFileCount,
60
60
  generateVibesBatch,
61
61
  } from "./working-vibes.ts";
62
+ import { setupTpsTracker } from "./tps.ts";
62
63
 
63
64
  // ═══════════════════════════════════════════════════════════════════════════
64
65
  // Configuration
@@ -1175,6 +1176,8 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1175
1176
  }
1176
1177
 
1177
1178
  // Track session start
1179
+ setupTpsTracker(pi);
1180
+
1178
1181
  pi.on("session_start", async (event, ctx) => {
1179
1182
  shellSession?.dispose();
1180
1183
  shellSession = null;
@@ -0,0 +1,286 @@
1
+ /**
2
+ * TPS Tracker — tokens-per-second tracking merged into the powerline footer.
3
+ *
4
+ * Writes live and final TPS stats to `ctx.ui.setStatus("tps", ...)`, which the
5
+ * powerline `extension_statuses` segment surfaces in the status bar (and the
6
+ * `setExtensionStatus` repaint hook turns into a re-render automatically).
7
+ *
8
+ * Tracks:
9
+ * - Main model output tokens vs. streaming wall time (excludes first-token
10
+ * latency and tool execution gaps).
11
+ * - pi-subagents child output tokens via tool_execution_* events, aggregated
12
+ * into a combined "main + subagents" TPS at agent_end.
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+
17
+ export function setupTpsTracker(pi: ExtensionAPI): void {
18
+ /** Timestamp when the current assistant message event started. Used as a fallback. */
19
+ let messageStart: number | null = null;
20
+ /** Timestamp of the first streamed output delta for the current assistant message. */
21
+ let streamStart: number | null = null;
22
+ /** Estimated streamed output tokens for live display before providers report final usage. */
23
+ let estimatedStreamedTokens = 0;
24
+ /** Cumulative official output tokens across all assistant messages in this agent run. */
25
+ let totalOutputTokens = 0;
26
+ /** Cumulative time (ms) spent actually streaming output deltas (excludes tool execution and first-token latency). */
27
+ let totalStreamMs = 0;
28
+ /** Subagent tool-call start times, used to include pi-subagents child output in aggregate TPS. */
29
+ const subagentStarts = new Map<string, number>();
30
+ /** Latest cumulative foreground subagent tokens seen during tool_execution_update. */
31
+ const subagentLiveTokens = new Map<string, number>();
32
+ /** Cumulative subagent output tokens in this agent run. */
33
+ let totalSubagentTokens = 0;
34
+ /** Cumulative subagent wall time (ms) in this agent run. */
35
+ let totalSubagentMs = 0;
36
+ /** Completed subagent call summaries in this agent run. */
37
+ const subagentSummaries: Array<{ tokens: number; ms: number; tps: number }> = [];
38
+
39
+ function asRecord(value: unknown): Record<string, unknown> | null {
40
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
41
+ }
42
+
43
+ function numberValue(value: unknown): number {
44
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
45
+ }
46
+
47
+ function getDetailsResult(value: unknown): Record<string, unknown> {
48
+ const result = asRecord(value);
49
+ const details = asRecord(result?.details);
50
+ return details ?? result ?? {};
51
+ }
52
+
53
+ function getResults(value: unknown): Record<string, unknown>[] {
54
+ const details = getDetailsResult(value);
55
+ return Array.isArray(details.results)
56
+ ? details.results.map(asRecord).filter((item): item is Record<string, unknown> => !!item)
57
+ : [];
58
+ }
59
+
60
+ function generatedTokensFromUsage(usage: Record<string, unknown> | null): number {
61
+ return numberValue(usage?.output ?? usage?.outputTokens ?? usage?.completionTokens ?? usage?.completion_tokens);
62
+ }
63
+
64
+ function generatedTokensFromMessage(message: { usage?: unknown; content?: unknown }): number {
65
+ const usageTokens = generatedTokensFromUsage(asRecord(message.usage));
66
+ if (usageTokens > 0) return usageTokens;
67
+
68
+ const content = Array.isArray(message.content) ? message.content : [];
69
+ const chars = content.reduce((sum, block) => {
70
+ const item = asRecord(block);
71
+ if (!item) return sum;
72
+ if (item.type === "text") return sum + String(item.text ?? "").length;
73
+ if (item.type === "thinking") return sum + String(item.thinking ?? "").length;
74
+ if (item.type === "toolCall") return sum + String(item.name ?? "").length + JSON.stringify(item.arguments ?? {}).length;
75
+ return sum;
76
+ }, 0);
77
+ return Math.ceil(chars / 4);
78
+ }
79
+
80
+ function outputTokensFromResult(value: unknown): number {
81
+ const results = getResults(value);
82
+ return results.reduce((sum, result) => sum + generatedTokensFromUsage(asRecord(result.usage)), 0);
83
+ }
84
+
85
+ function liveTokensFromResult(value: unknown): number {
86
+ return subagentProgressItems(value).reduce((sum, item) => sum + item.tokens, 0);
87
+ }
88
+
89
+ function subagentProgressItems(value: unknown): Array<{ name: string; tokens: number; status?: string }> {
90
+ const details = getDetailsResult(value);
91
+ const progress = Array.isArray(details.progress) ? details.progress : undefined;
92
+ const resultItems = getResults(value).map((result, index) => {
93
+ const progress = asRecord(result.progress);
94
+ const usage = asRecord(result.usage);
95
+ return {
96
+ name: String(result.agent || progress?.agent || `agent${index + 1}`),
97
+ tokens: generatedTokensFromUsage(usage) || generatedTokensFromUsage(progress),
98
+ status: typeof progress?.status === "string" ? progress.status : undefined,
99
+ };
100
+ });
101
+ if (!progress || resultItems.some((item) => item.tokens > 0)) return resultItems;
102
+
103
+ return progress.map((entry, index) => {
104
+ const item = asRecord(entry) ?? {};
105
+ return {
106
+ name: String(item.agent || `agent${index + 1}`),
107
+ tokens: generatedTokensFromUsage(item),
108
+ status: typeof item.status === "string" ? item.status : undefined,
109
+ };
110
+ });
111
+ }
112
+
113
+ function subagentModeLabel(value: unknown): string {
114
+ const details = getDetailsResult(value);
115
+ const mode = typeof details.mode === "string" ? details.mode : "subagents";
116
+ return mode === "parallel" ? "parallel" : mode === "chain" ? "chain" : "subagent";
117
+ }
118
+
119
+ pi.on("agent_start", async (_event, ctx) => {
120
+ totalOutputTokens = 0;
121
+ totalStreamMs = 0;
122
+ totalSubagentTokens = 0;
123
+ totalSubagentMs = 0;
124
+ subagentSummaries.length = 0;
125
+ messageStart = null;
126
+ streamStart = null;
127
+ estimatedStreamedTokens = 0;
128
+ subagentStarts.clear();
129
+ subagentLiveTokens.clear();
130
+ const theme = ctx.ui.theme;
131
+ ctx.ui.setStatus("tps", theme.fg("success", "generating..."));
132
+ });
133
+
134
+ pi.on("message_start", async (event) => {
135
+ if (event.message.role !== "assistant") return;
136
+ messageStart = Date.now();
137
+ streamStart = null;
138
+ estimatedStreamedTokens = 0;
139
+ });
140
+
141
+ pi.on("message_update", async (event, ctx) => {
142
+ if (event.message.role !== "assistant") return;
143
+
144
+ const streamEvent = event.assistantMessageEvent;
145
+ const isOutputDelta =
146
+ streamEvent.type === "text_delta" ||
147
+ streamEvent.type === "thinking_delta" ||
148
+ streamEvent.type === "toolcall_delta";
149
+
150
+ if (!isOutputDelta) return;
151
+
152
+ const now = Date.now();
153
+ streamStart ??= now;
154
+ estimatedStreamedTokens += Math.max(0, streamEvent.delta.length / 4);
155
+
156
+ const elapsed = (now - streamStart) / 1000;
157
+ const officialTokens = generatedTokensFromUsage(asRecord(event.message.usage));
158
+ const currentTokens = officialTokens > 0 ? officialTokens : estimatedStreamedTokens;
159
+
160
+ if (elapsed > 0 && currentTokens > 0) {
161
+ const tps = Math.round(currentTokens / elapsed);
162
+ const tokenLabel = officialTokens > 0
163
+ ? `${officialTokens} tok`
164
+ : `~${Math.round(estimatedStreamedTokens)} tok`;
165
+ const theme = ctx.ui.theme;
166
+ ctx.ui.setStatus(
167
+ "tps",
168
+ `${theme.fg("accent", `${tps} tok/s`)}`,
169
+ );
170
+ }
171
+ });
172
+
173
+ pi.on("message_end", async (event) => {
174
+ if (event.message.role !== "assistant") return;
175
+
176
+ const messageTokens = generatedTokensFromMessage(event.message);
177
+ const timingStart = streamStart ?? messageStart;
178
+ if (!timingStart || messageTokens <= 0) {
179
+ messageStart = null;
180
+ streamStart = null;
181
+ estimatedStreamedTokens = 0;
182
+ return;
183
+ }
184
+
185
+ totalOutputTokens += messageTokens;
186
+ totalStreamMs += Math.max(0, Date.now() - timingStart);
187
+
188
+ messageStart = null;
189
+ streamStart = null;
190
+ estimatedStreamedTokens = 0;
191
+ });
192
+
193
+ pi.on("tool_execution_start", async (event) => {
194
+ if (event.toolName !== "subagent") return;
195
+ subagentStarts.set(event.toolCallId, Date.now());
196
+ });
197
+
198
+ pi.on("tool_execution_update", async (event, ctx) => {
199
+ if (event.toolName !== "subagent") return;
200
+
201
+ const start = subagentStarts.get(event.toolCallId);
202
+ if (!start) return;
203
+
204
+ const items = subagentProgressItems(event.partialResult);
205
+ const tokens = items.reduce((sum, item) => sum + item.tokens, 0);
206
+ subagentLiveTokens.set(event.toolCallId, tokens);
207
+ const elapsed = (Date.now() - start) / 1000;
208
+ if (tokens <= 0 || elapsed <= 0) return;
209
+
210
+ const tps = Math.round(tokens / elapsed);
211
+ const mode = subagentModeLabel(event.partialResult);
212
+ const agentSummary = items
213
+ .filter((item) => item.tokens > 0 || item.status === "running")
214
+ .map((item) => `${item.name} ${Math.round(item.tokens / elapsed)} t/s`)
215
+ .join(" ");
216
+ const theme = ctx.ui.theme;
217
+ ctx.ui.setStatus("tps", `${theme.fg("accent", `main+${mode} ${tps} t/s`)} ${theme.fg("success", `(${agentSummary || `${tps} t/s`})`)}`);
218
+ });
219
+
220
+ pi.on("tool_execution_end", async (event, ctx) => {
221
+ if (event.toolName !== "subagent") return;
222
+
223
+ const start = subagentStarts.get(event.toolCallId);
224
+ subagentStarts.delete(event.toolCallId);
225
+ const liveTokens = subagentLiveTokens.get(event.toolCallId) ?? liveTokensFromResult(event.result);
226
+ subagentLiveTokens.delete(event.toolCallId);
227
+ if (!start || event.isError) return;
228
+
229
+ const outputTokens = outputTokensFromResult(event.result);
230
+ const displayTokens = outputTokens || liveTokens;
231
+ const mode = subagentModeLabel(event.result);
232
+ const elapsedMs = Math.max(0, Date.now() - start);
233
+ if (displayTokens <= 0 || elapsedMs <= 0) return;
234
+ const elapsedSeconds = elapsedMs / 1000;
235
+ const items = subagentProgressItems(event.result);
236
+ const agentSummary = items
237
+ .filter((item) => item.tokens > 0)
238
+ .map((item) => `${item.name} ${Math.round(item.tokens / elapsedSeconds)} t/s`)
239
+ .join(" ");
240
+
241
+ totalOutputTokens += displayTokens;
242
+ totalStreamMs += elapsedMs;
243
+ totalSubagentTokens += displayTokens;
244
+ totalSubagentMs += elapsedMs;
245
+
246
+ const tps = Math.round(displayTokens / elapsedSeconds);
247
+ subagentSummaries.push({ tokens: displayTokens, ms: elapsedMs, tps });
248
+ const theme = ctx.ui.theme;
249
+ ctx.ui.notify(`${theme.fg("success", "✓")} ${theme.fg("accent", `${tps} t/s`)}`, "info");
250
+ ctx.ui.setStatus(
251
+ "tps",
252
+ `${theme.fg("accent", `main+${mode} ${tps} t/s`)} ${theme.fg("success", `(${agentSummary || `${tps} t/s`})`)}`,
253
+ );
254
+ });
255
+
256
+ pi.on("agent_end", async (_event, ctx) => {
257
+ const elapsed = totalStreamMs / 1000;
258
+ const tps = totalOutputTokens > 0 && elapsed > 0 ? Math.round(totalOutputTokens / elapsed) : 0;
259
+
260
+ const theme = ctx.ui.theme;
261
+ const icon = theme.fg("success", "✓");
262
+ const tpsLabel = tps > 0
263
+ ? theme.fg("accent", `${tps} t/s`)
264
+ : theme.fg("success", "N/A");
265
+ const mainTokens = totalOutputTokens - totalSubagentTokens;
266
+ const mainMs = totalStreamMs - totalSubagentMs;
267
+ const mainTps = mainTokens > 0 && mainMs > 0 ? Math.round(mainTokens / (mainMs / 1000)) : 0;
268
+ const subagentTps = totalSubagentTokens > 0 && totalSubagentMs > 0
269
+ ? Math.round(totalSubagentTokens / (totalSubagentMs / 1000))
270
+ : 0;
271
+ const mainDetail = mainTps > 0 ? theme.fg("success", ` | main ${mainTps} t/s`) : "";
272
+ const subagentDetail = subagentTps > 0
273
+ ? theme.fg("success", ` | subagents ${subagentTps} t/s (${subagentSummaries.length} call${subagentSummaries.length === 1 ? "" : "s"})`)
274
+ : "";
275
+
276
+ ctx.ui.notify(`${icon} total ${tpsLabel}${mainDetail}${subagentDetail}`, "info");
277
+ const finalMainStatus = mainTps > 0 ? `main ${mainTps} t/s` : "main N/A";
278
+ const finalSubagentStatus = totalSubagentTokens > 0 && totalSubagentMs > 0
279
+ ? `subagents ${Math.round(totalSubagentTokens / (totalSubagentMs / 1000))} t/s`
280
+ : "";
281
+ ctx.ui.setStatus(
282
+ "tps",
283
+ `${theme.fg("accent", `done ${tps} t/s`)} ${theme.fg("success", `(${[finalMainStatus, finalSubagentStatus].filter(Boolean).join(" ")})`)}`,
284
+ );
285
+ });
286
+ }
@@ -22,12 +22,13 @@ export interface LoadedCounts {
22
22
  // ═══════════════════════════════════════════════════════════════════════════
23
23
 
24
24
  const PI_LOGO = [
25
- "██████████ ",
26
- "████ ████ ",
27
- "████ ████ ",
28
- "████████ ████",
29
- "████ ████",
30
- "████ ████",
25
+ "███ ███ █ ███ ███ ███ █",
26
+ "█ █ █ █ █ █ █ █",
27
+ "█ █ █ █ █ █ █ █",
28
+ "███ ███ █ ███ ███ ███ █",
29
+ " █ █ █ █ █ █ █ █",
30
+ " █ █ █ █ █ █ █ █",
31
+ "███ ███ ███ ███ ███ █ █ █",
31
32
  ];
32
33
 
33
34
  const GRADIENT_COLORS = [
@@ -89,7 +90,7 @@ interface WelcomeData {
89
90
 
90
91
  function buildLeftColumn(data: WelcomeData, colWidth: number): string[] {
91
92
  const logoColored = PI_LOGO.map((line) => gradientLine(line));
92
-
93
+
93
94
  return [
94
95
  "",
95
96
  centerText(bold("Welcome back!"), colWidth),
@@ -104,7 +105,7 @@ function buildLeftColumn(data: WelcomeData, colWidth: number): string[] {
104
105
  function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
105
106
  const hChar = "─";
106
107
  const separator = ` ${dim(hChar.repeat(colWidth - 2))}`;
107
-
108
+
108
109
  // Session lines
109
110
  const sessionLines: string[] = [];
110
111
  if (data.recentSessions.length === 0) {
@@ -116,12 +117,12 @@ function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
116
117
  );
117
118
  }
118
119
  }
119
-
120
+
120
121
  // Loaded counts lines
121
122
  const countLines: string[] = [];
122
123
  const { contextFiles, extensions, skills, promptTemplates } = data.loadedCounts;
123
124
  const itemPrefix = dim("- ");
124
-
125
+
125
126
  if (contextFiles > 0 || extensions > 0 || skills > 0 || promptTemplates > 0) {
126
127
  if (contextFiles > 0) {
127
128
  countLines.push(` ${itemPrefix}${fgOnly("gitClean", `${contextFiles}`)} context file${contextFiles !== 1 ? "s" : ""}`);
@@ -138,7 +139,7 @@ function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
138
139
  } else {
139
140
  countLines.push(` ${dim("No extensions loaded")}`);
140
141
  }
141
-
142
+
142
143
  return [
143
144
  ` ${bold(fgOnly("accent", "Tips"))}`,
144
145
  ` ${dim("/")} for commands`,
@@ -155,46 +156,46 @@ function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
155
156
  }
156
157
 
157
158
  function renderWelcomeBox(
158
- data: WelcomeData,
159
- termWidth: number,
159
+ data: WelcomeData,
160
+ termWidth: number,
160
161
  bottomLine: string,
161
162
  ): string[] {
162
163
  // Minimum width for two-column layout: leftCol(26) + separator(3) + minRightCol(15) = 44
163
164
  const minLayoutWidth = 44;
164
-
165
+
165
166
  // If terminal is too narrow for the layout, return empty (skip welcome box)
166
167
  if (termWidth < minLayoutWidth) {
167
168
  return [];
168
169
  }
169
-
170
+
170
171
  const minWidth = 76;
171
172
  const maxWidth = 96;
172
173
  // Clamp to termWidth to prevent crash on narrow terminals
173
174
  const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
174
175
  const leftCol = 26;
175
176
  const rightCol = Math.max(1, boxWidth - leftCol - 3); // Ensure rightCol is at least 1
176
-
177
+
177
178
  const hChar = "─";
178
179
  const v = dim("│");
179
180
  const tl = dim("╭");
180
181
  const tr = dim("╮");
181
182
  const bl = dim("╰");
182
183
  const br = dim("╯");
183
-
184
+
184
185
  const leftLines = buildLeftColumn(data, leftCol);
185
186
  const rightLines = buildRightColumn(data, rightCol);
186
-
187
+
187
188
  const lines: string[] = [];
188
-
189
+
189
190
  // Top border with title
190
- const title = " pi agent ";
191
+ const title = "SelesaiCode";
191
192
  const titlePrefix = dim(hChar.repeat(3));
192
193
  const titleStyled = titlePrefix + fgOnly("model", title);
193
194
  const titleVisLen = 3 + visibleWidth(title);
194
195
  const afterTitle = boxWidth - 2 - titleVisLen;
195
196
  const afterTitleText = afterTitle > 0 ? dim(hChar.repeat(afterTitle)) : "";
196
197
  lines.push(tl + titleStyled + afterTitleText + tr);
197
-
198
+
198
199
  // Content rows
199
200
  const maxRows = Math.max(leftLines.length, rightLines.length);
200
201
  for (let i = 0; i < maxRows; i++) {
@@ -202,10 +203,10 @@ function renderWelcomeBox(
202
203
  const right = fitToWidth(rightLines[i] ?? "", rightCol);
203
204
  lines.push(v + left + v + right + v);
204
205
  }
205
-
206
+
206
207
  // Bottom border
207
208
  lines.push(bl + bottomLine + br);
208
-
209
+
209
210
  return lines;
210
211
  }
211
212
 
@@ -214,7 +215,7 @@ function renderWelcomeBox(
214
215
  // ═══════════════════════════════════════════════════════════════════════════
215
216
 
216
217
  /**
217
- * Welcome overlay component for pi agent.
218
+ * Welcome overlay component for SelesaiCode.
218
219
  * Displays a branded splash screen with logo, tips, and loaded counts.
219
220
  */
220
221
  export class WelcomeComponent implements Component {
@@ -242,12 +243,12 @@ export class WelcomeComponent implements Component {
242
243
  if (termWidth < minLayoutWidth) {
243
244
  return [];
244
245
  }
245
-
246
+
246
247
  const minWidth = 76;
247
248
  const maxWidth = 96;
248
249
  // Clamp to termWidth to prevent crash on narrow terminals
249
250
  const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
250
-
251
+
251
252
  // Bottom line with countdown
252
253
  const countdownText = ` Press any key to continue (${this.countdown}s) `;
253
254
  const countdownStyled = dim(countdownText);
@@ -256,10 +257,10 @@ export class WelcomeComponent implements Component {
256
257
  const leftPad = Math.floor((bottomContentWidth - countdownVisLen) / 2);
257
258
  const rightPad = bottomContentWidth - countdownVisLen - leftPad;
258
259
  const hChar = "─";
259
- const bottomLine = dim(hChar.repeat(Math.max(0, leftPad))) +
260
- countdownStyled +
260
+ const bottomLine = dim(hChar.repeat(Math.max(0, leftPad))) +
261
+ countdownStyled +
261
262
  dim(hChar.repeat(Math.max(0, rightPad)));
262
-
263
+
263
264
  return renderWelcomeBox(this.data, termWidth, bottomLine);
264
265
  }
265
266
  }
@@ -288,18 +289,18 @@ export class WelcomeHeader implements Component {
288
289
  if (termWidth < minLayoutWidth) {
289
290
  return [];
290
291
  }
291
-
292
+
292
293
  const minWidth = 76;
293
294
  const maxWidth = 96;
294
295
  // Clamp to termWidth to prevent crash on narrow terminals
295
296
  const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
296
297
  const hChar = "─";
297
-
298
+
298
299
  // Bottom line with column separator (leftCol=26, rightCol=boxWidth-29)
299
300
  const leftCol = 26;
300
301
  const rightCol = Math.max(1, boxWidth - leftCol - 3);
301
302
  const bottomLine = dim(hChar.repeat(leftCol)) + dim("┴") + dim(hChar.repeat(rightCol));
302
-
303
+
303
304
  const lines = renderWelcomeBox(this.data, termWidth, bottomLine);
304
305
  if (lines.length > 0) {
305
306
  lines.push(""); // Add empty line for spacing only if we rendered content
@@ -335,7 +336,7 @@ function logDiscoveryError(scope: string, error: unknown): void {
335
336
  export function discoverLoadedCounts(): LoadedCounts {
336
337
  const homeDir = process.env.HOME || process.env.USERPROFILE || osHomedir();
337
338
  const cwd = process.cwd();
338
-
339
+
339
340
  let contextFiles = 0;
340
341
  let extensions = 0;
341
342
  let skills = 0;
@@ -348,7 +349,7 @@ export function discoverLoadedCounts(): LoadedCounts {
348
349
  join(cwd, ".pi", "AGENTS.md"),
349
350
  join(cwd, ".claude", "AGENTS.md"),
350
351
  ];
351
-
352
+
352
353
  for (const path of agentsMdPaths) {
353
354
  if (existsSync(path)) contextFiles++;
354
355
  }
@@ -463,9 +464,9 @@ export function discoverLoadedCounts(): LoadedCounts {
463
464
  join(cwd, ".pi", "skills"),
464
465
  join(cwd, "skills"),
465
466
  ];
466
-
467
+
467
468
  const countedSkills = new Set<string>();
468
-
469
+
469
470
  for (const dir of skillDirs) {
470
471
  if (existsSync(dir)) {
471
472
  try {
@@ -497,9 +498,9 @@ export function discoverLoadedCounts(): LoadedCounts {
497
498
  join(cwd, ".pi", "commands"),
498
499
  join(cwd, ".claude", "commands"),
499
500
  ];
500
-
501
+
501
502
  const countedTemplates = new Set<string>();
502
-
503
+
503
504
  function countTemplatesInDir(dir: string) {
504
505
  if (!existsSync(dir)) return;
505
506
  try {
@@ -525,7 +526,7 @@ export function discoverLoadedCounts(): LoadedCounts {
525
526
  logDiscoveryError(`Failed to scan prompt template dir ${dir}`, error);
526
527
  }
527
528
  }
528
-
529
+
529
530
  for (const dir of templateDirs) {
530
531
  countTemplatesInDir(dir);
531
532
  }
@@ -538,14 +539,14 @@ export function discoverLoadedCounts(): LoadedCounts {
538
539
  */
539
540
  export function getRecentSessions(maxCount: number = 3): RecentSession[] {
540
541
  const homeDir = process.env.HOME || process.env.USERPROFILE || osHomedir();
541
-
542
+
542
543
  const sessionsDirs = [
543
544
  join(homeDir, ".pi", "agent", "sessions"),
544
545
  join(homeDir, ".pi", "sessions"),
545
546
  ];
546
-
547
+
547
548
  const sessions: { name: string; mtime: number }[] = [];
548
-
549
+
549
550
  function scanDir(dir: string) {
550
551
  if (!existsSync(dir)) return;
551
552
  try {
@@ -573,15 +574,15 @@ export function getRecentSessions(maxCount: number = 3): RecentSession[] {
573
574
  logDiscoveryError(`Failed to scan sessions dir ${dir}`, error);
574
575
  }
575
576
  }
576
-
577
+
577
578
  for (const sessionsDir of sessionsDirs) {
578
579
  scanDir(sessionsDir);
579
580
  }
580
-
581
+
581
582
  if (sessions.length === 0) return [];
582
-
583
+
583
584
  sessions.sort((a, b) => b.mtime - a.mtime);
584
-
585
+
585
586
  const seen = new Set<string>();
586
587
  const uniqueSessions: typeof sessions = [];
587
588
  for (const s of sessions) {