killeros 2.0.4 → 2.0.6

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.
@@ -20,6 +20,12 @@ export interface InitRuntime {
20
20
 
21
21
  export type GoalStatus = "active" | "paused" | "blocked" | "complete";
22
22
 
23
+ export interface GoalBlockerAudit {
24
+ key: string;
25
+ streak: number;
26
+ lastTurn: number;
27
+ }
28
+
23
29
  export interface GoalState {
24
30
  version: 1;
25
31
  revision: number;
@@ -34,6 +40,7 @@ export interface GoalState {
34
40
  baselineTokens: number;
35
41
  result?: string;
36
42
  resumeAfterManualCompaction?: true;
43
+ blockerAudit?: GoalBlockerAudit;
37
44
  }
38
45
 
39
46
  export interface GoalRuntime {
@@ -11,6 +11,7 @@ import {
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import {
13
13
  Container,
14
+ CURSOR_MARKER,
14
15
  Text,
15
16
  truncateToWidth,
16
17
  visibleWidth,
@@ -43,6 +44,13 @@ const STARTUP_TIPS = [
43
44
  "Run /notification to enable a terminal bell when work settles.",
44
45
  ] as const;
45
46
 
47
+ const EDITOR_SUGGESTIONS = [
48
+ 'Try "how does <filepath> work?"',
49
+ 'Try "find edge cases in <filepath>"',
50
+ 'Try "simplify <filepath> without changing behavior"',
51
+ 'Try "write tests for <filepath>"',
52
+ ] as const;
53
+
46
54
  export function resolveGitBranch(cwd: string): Promise<string | undefined> {
47
55
  return new Promise((resolve) => {
48
56
  execFile(
@@ -66,13 +74,26 @@ export function resolveGitBranch(cwd: string): Promise<string | undefined> {
66
74
  });
67
75
  }
68
76
 
69
- function shuffledTips(): string[] {
70
- const tips = [...STARTUP_TIPS];
71
- for (let index = tips.length - 1; index > 0; index -= 1) {
77
+ function shuffledDeck(values: readonly string[]): string[] {
78
+ const deck = [...values];
79
+ for (let index = deck.length - 1; index > 0; index -= 1) {
72
80
  const swapIndex = Math.floor(Math.random() * (index + 1));
73
- [tips[index], tips[swapIndex]] = [tips[swapIndex]!, tips[index]!];
81
+ [deck[index], deck[swapIndex]] = [deck[swapIndex]!, deck[index]!];
74
82
  }
75
- return tips;
83
+ return deck;
84
+ }
85
+
86
+ let tipDeck: string[] = [];
87
+ let editorSuggestionDeck: string[] = [];
88
+
89
+ function nextStartupTip(): string {
90
+ if (tipDeck.length === 0) tipDeck = shuffledDeck(STARTUP_TIPS);
91
+ return tipDeck.pop() ?? STARTUP_TIPS[0];
92
+ }
93
+
94
+ function nextEditorSuggestion(): string {
95
+ if (editorSuggestionDeck.length === 0) editorSuggestionDeck = shuffledDeck(EDITOR_SUGGESTIONS);
96
+ return editorSuggestionDeck.pop() ?? EDITOR_SUGGESTIONS[0];
76
97
  }
77
98
 
78
99
  function compactBoxLine(content: string, width: number, theme: Theme): string {
@@ -167,16 +188,19 @@ function isScrolledTopBorder(line: string): boolean {
167
188
  class PiCodeEditor extends CustomEditor {
168
189
  private readonly appKeybindings: KeybindingsManager;
169
190
  private readonly runtimeTheme: Theme;
191
+ private readonly suggestion: string;
170
192
 
171
193
  constructor(
172
194
  tui: TUI,
173
195
  theme: EditorTheme,
174
196
  appKeybindings: KeybindingsManager,
175
197
  runtimeTheme: Theme,
198
+ suggestion: string,
176
199
  ) {
177
200
  super(tui, theme, appKeybindings);
178
201
  this.appKeybindings = appKeybindings;
179
202
  this.runtimeTheme = runtimeTheme;
203
+ this.suggestion = suggestion;
180
204
  }
181
205
 
182
206
  override handleInput(data: string): void {
@@ -220,8 +244,16 @@ class PiCodeEditor extends CustomEditor {
220
244
  }
221
245
 
222
246
  for (let index = 1; index < bottomBorderIndex; index += 1) {
223
- const prefix = index === 1 && !isScrolledHeader ? gray("❯ ") : " ";
224
- framed.push(`${prefix}${padRight(lines[index] ?? "", innerWidth)}`);
247
+ const isPromptLine = index === 1 && !isScrolledHeader;
248
+ const prefix = isPromptLine ? gray("❯\u00A0") : " ";
249
+ let content = lines[index] ?? "";
250
+ if (isPromptLine && this.getText() === "") {
251
+ const first = this.suggestion.slice(0, 1);
252
+ const rest = this.suggestion.slice(1);
253
+ const cursorMarker = this.focused ? CURSOR_MARKER : "";
254
+ content = `${cursorMarker}\x1B[7m${gray(first)}\x1B[27m${gray(rest)}`;
255
+ }
256
+ framed.push(`${prefix}${padRight(content, innerWidth)}`);
225
257
  }
226
258
 
227
259
  const bottom = stripAnsi(lines[bottomBorderIndex] ?? "");
@@ -258,11 +290,6 @@ export function registerShellUi(pi: ExtensionAPI): void {
258
290
  let activityDeck: string[] = [];
259
291
  let lastActivityWord: string | undefined;
260
292
  let activityTimer: ReturnType<typeof setInterval> | undefined;
261
- let tipDeck: string[] = [];
262
- const nextStartupTip = (): string => {
263
- if (tipDeck.length === 0) tipDeck = shuffledTips();
264
- return tipDeck.pop() ?? STARTUP_TIPS[0];
265
- };
266
293
  const refillActivityDeck = (): void => {
267
294
  activityDeck = [...ACTIVITY_WORDS];
268
295
  for (let index = activityDeck.length - 1; index > 0; index -= 1) {
@@ -302,8 +329,9 @@ export function registerShellUi(pi: ExtensionAPI): void {
302
329
  ctx.ui.setHiddenThinkingLabel("└ Thinking…");
303
330
  const existingEditorFactory = ctx.ui.getEditorComponent?.();
304
331
  if (!existingEditorFactory || existingEditorFactory === killerosEditorFactory) {
332
+ const editorSuggestion = nextEditorSuggestion();
305
333
  killerosEditorFactory = (tui, editorTheme, keybindings) =>
306
- new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme);
334
+ new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme, editorSuggestion);
307
335
  ctx.ui.setEditorComponent(killerosEditorFactory);
308
336
  }
309
337
  } catch (error) {
@@ -0,0 +1,80 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+
4
+ const WORKED_FOR_ENTRY_TYPE = "killeros-worked-for";
5
+ const WORKED_FOR_ENTRY_VERSION = 1;
6
+
7
+ interface WorkedForEntryData {
8
+ version: typeof WORKED_FOR_ENTRY_VERSION;
9
+ milliseconds: number;
10
+ }
11
+
12
+ export function formatWorkedForDuration(milliseconds: number): string {
13
+ const boundedMilliseconds = Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : 0;
14
+ const totalSeconds = Math.max(1, Math.floor(boundedMilliseconds / 1_000));
15
+ if (totalSeconds < 60) return `${totalSeconds}s`;
16
+
17
+ const totalMinutes = Math.floor(totalSeconds / 60);
18
+ if (totalMinutes < 60) {
19
+ return `${totalMinutes}m ${(totalSeconds % 60).toString().padStart(2, "0")}s`;
20
+ }
21
+
22
+ return `${Math.floor(totalMinutes / 60)}h ${(totalMinutes % 60).toString().padStart(2, "0")}m`;
23
+ }
24
+
25
+ function isWorkedForEntryData(data: unknown): data is WorkedForEntryData {
26
+ if (!data || typeof data !== "object") return false;
27
+ const candidate = data as Partial<WorkedForEntryData>;
28
+ return candidate.version === WORKED_FOR_ENTRY_VERSION
29
+ && typeof candidate.milliseconds === "number"
30
+ && Number.isFinite(candidate.milliseconds)
31
+ && candidate.milliseconds >= 0;
32
+ }
33
+
34
+ function errorMessage(error: unknown): string {
35
+ return error instanceof Error ? error.message : String(error);
36
+ }
37
+
38
+ export function registerWorkedFor(
39
+ pi: ExtensionAPI,
40
+ now: () => number = Date.now,
41
+ ): void {
42
+ let startedAt: number | undefined;
43
+
44
+ pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, _options, theme) => {
45
+ if (!isWorkedForEntryData(entry.data)) return undefined;
46
+ return new Text(
47
+ theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(entry.data.milliseconds)}`),
48
+ 0,
49
+ 0,
50
+ );
51
+ });
52
+
53
+ pi.on("session_start", () => {
54
+ startedAt = undefined;
55
+ });
56
+
57
+ pi.on("agent_start", (_event, ctx) => {
58
+ if (ctx.mode !== "tui" || startedAt !== undefined) return;
59
+ startedAt = now();
60
+ });
61
+
62
+ pi.on("agent_settled", (_event, ctx) => {
63
+ if (ctx.mode !== "tui" || startedAt === undefined) return;
64
+ if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
65
+ const milliseconds = Math.max(0, now() - startedAt);
66
+ startedAt = undefined;
67
+ try {
68
+ pi.appendEntry<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, {
69
+ version: WORKED_FOR_ENTRY_VERSION,
70
+ milliseconds,
71
+ });
72
+ } catch (error) {
73
+ ctx.ui.notify(`Worked-for timing could not be saved: ${errorMessage(error)}`, "error");
74
+ }
75
+ });
76
+
77
+ pi.on("session_shutdown", () => {
78
+ startedAt = undefined;
79
+ });
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [