pi-extensions 0.1.9

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 (135) hide show
  1. package/.ralph/import-cc-codex.md +31 -0
  2. package/.ralph/import-cc-codex.state.json +14 -0
  3. package/.ralph/mario-not-impl.md +69 -0
  4. package/.ralph/mario-not-impl.state.json +14 -0
  5. package/.ralph/mario-not-spec.md +163 -0
  6. package/.ralph/mario-not-spec.state.json +14 -0
  7. package/LICENSE +21 -0
  8. package/README.md +65 -0
  9. package/RELEASING.md +34 -0
  10. package/agent-guidance/CHANGELOG.md +4 -0
  11. package/agent-guidance/README.md +102 -0
  12. package/agent-guidance/agent-guidance.ts +147 -0
  13. package/agent-guidance/package.json +22 -0
  14. package/agent-guidance/setup.sh +75 -0
  15. package/agent-guidance/templates/CLAUDE.md +5 -0
  16. package/agent-guidance/templates/CODEX.md +92 -0
  17. package/agent-guidance/templates/GEMINI.md +5 -0
  18. package/arcade/CHANGELOG.md +4 -0
  19. package/arcade/README.md +85 -0
  20. package/arcade/assets/picman.png +0 -0
  21. package/arcade/assets/ping.png +0 -0
  22. package/arcade/assets/spice-invaders.png +0 -0
  23. package/arcade/assets/tetris.png +0 -0
  24. package/arcade/mario-not/README.md +30 -0
  25. package/arcade/mario-not/boss.js +103 -0
  26. package/arcade/mario-not/camera.js +59 -0
  27. package/arcade/mario-not/collision.js +91 -0
  28. package/arcade/mario-not/colors.js +36 -0
  29. package/arcade/mario-not/constants.js +97 -0
  30. package/arcade/mario-not/core.js +39 -0
  31. package/arcade/mario-not/death.js +77 -0
  32. package/arcade/mario-not/effects.js +84 -0
  33. package/arcade/mario-not/enemies.js +31 -0
  34. package/arcade/mario-not/engine.js +171 -0
  35. package/arcade/mario-not/fireballs.js +98 -0
  36. package/arcade/mario-not/items.js +24 -0
  37. package/arcade/mario-not/levels.js +403 -0
  38. package/arcade/mario-not/logic.js +104 -0
  39. package/arcade/mario-not/mario-not.ts +297 -0
  40. package/arcade/mario-not/player.js +244 -0
  41. package/arcade/mario-not/render.js +257 -0
  42. package/arcade/mario-not/spec.md +548 -0
  43. package/arcade/mario-not/state.js +246 -0
  44. package/arcade/mario-not/tests/e2e.test.js +855 -0
  45. package/arcade/mario-not/tests/engine.test.js +888 -0
  46. package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
  47. package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
  48. package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
  49. package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
  50. package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
  51. package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
  52. package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
  53. package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
  54. package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
  55. package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
  56. package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
  57. package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
  58. package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
  59. package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
  60. package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
  61. package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
  62. package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
  63. package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
  64. package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
  65. package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
  66. package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
  67. package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
  68. package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
  69. package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
  70. package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
  71. package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
  72. package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
  73. package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
  74. package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
  75. package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
  76. package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
  77. package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
  78. package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
  79. package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
  80. package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
  81. package/arcade/mario-not/tiles.js +79 -0
  82. package/arcade/mario-not/tsconfig.json +14 -0
  83. package/arcade/mario-not/types.js +225 -0
  84. package/arcade/package.json +26 -0
  85. package/arcade/picman.ts +328 -0
  86. package/arcade/ping.ts +594 -0
  87. package/arcade/spice-invaders.ts +1104 -0
  88. package/arcade/tetris.ts +662 -0
  89. package/code-actions/CHANGELOG.md +4 -0
  90. package/code-actions/README.md +65 -0
  91. package/code-actions/actions.ts +107 -0
  92. package/code-actions/index.ts +148 -0
  93. package/code-actions/package.json +22 -0
  94. package/code-actions/search.ts +79 -0
  95. package/code-actions/snippets.ts +179 -0
  96. package/code-actions/ui.ts +120 -0
  97. package/files-widget/CHANGELOG.md +90 -0
  98. package/files-widget/DESIGN.md +452 -0
  99. package/files-widget/README.md +122 -0
  100. package/files-widget/TODO.md +141 -0
  101. package/files-widget/browser.ts +922 -0
  102. package/files-widget/comment.ts +5 -0
  103. package/files-widget/constants.ts +18 -0
  104. package/files-widget/demo.svg +1 -0
  105. package/files-widget/file-tree.ts +224 -0
  106. package/files-widget/file-viewer.ts +93 -0
  107. package/files-widget/git.ts +107 -0
  108. package/files-widget/index.ts +140 -0
  109. package/files-widget/input-utils.ts +3 -0
  110. package/files-widget/package.json +22 -0
  111. package/files-widget/types.ts +28 -0
  112. package/files-widget/utils.ts +26 -0
  113. package/files-widget/viewer.ts +424 -0
  114. package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
  115. package/import-cc-codex/spec.md +79 -0
  116. package/package.json +29 -0
  117. package/ralph-wiggum/CHANGELOG.md +7 -0
  118. package/ralph-wiggum/README.md +96 -0
  119. package/ralph-wiggum/SKILL.md +73 -0
  120. package/ralph-wiggum/index.ts +792 -0
  121. package/ralph-wiggum/package.json +25 -0
  122. package/raw-paste/CHANGELOG.md +7 -0
  123. package/raw-paste/README.md +52 -0
  124. package/raw-paste/index.ts +112 -0
  125. package/raw-paste/package.json +22 -0
  126. package/tab-status/CHANGELOG.md +4 -0
  127. package/tab-status/README.md +61 -0
  128. package/tab-status/assets/tab-status.png +0 -0
  129. package/tab-status/package.json +22 -0
  130. package/tab-status/tab-status.ts +179 -0
  131. package/usage-extension/CHANGELOG.md +17 -0
  132. package/usage-extension/README.md +120 -0
  133. package/usage-extension/index.ts +628 -0
  134. package/usage-extension/package.json +22 -0
  135. package/usage-extension/screenshot.png +0 -0
@@ -0,0 +1,424 @@
1
+ import type { Theme } from "@mariozechner/pi-coding-agent";
2
+ import { Key, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
+ import { readFileSync } from "node:fs";
4
+ import { relative } from "node:path";
5
+
6
+ import {
7
+ DEFAULT_VIEWER_HEIGHT,
8
+ MAX_VIEWER_HEIGHT,
9
+ MIN_PANEL_HEIGHT,
10
+ SEARCH_SCROLL_OFFSET,
11
+ VIEWER_SCROLL_MARGIN,
12
+ } from "./constants";
13
+ import { loadFileContent } from "./file-viewer";
14
+ import type { FileNode } from "./types";
15
+ import { isUntrackedStatus } from "./utils";
16
+ import { isPrintableChar } from "./input-utils";
17
+
18
+ export interface CommentPayload {
19
+ relPath: string;
20
+ lineRange: string;
21
+ ext: string;
22
+ selectedText: string;
23
+ }
24
+
25
+ export type ViewerAction =
26
+ | { type: "none" }
27
+ | { type: "close" }
28
+ | { type: "navigate"; direction: 1 | -1 };
29
+
30
+ type ViewerMode = "normal" | "select" | "search" | "comment";
31
+
32
+ interface ViewerState {
33
+ file: FileNode | null;
34
+ content: string[];
35
+ rawContent: string;
36
+ scroll: number;
37
+ diffMode: boolean;
38
+ mode: ViewerMode;
39
+ selectStart: number;
40
+ selectEnd: number;
41
+ commentText: string;
42
+ searchQuery: string;
43
+ searchMatches: number[];
44
+ searchIndex: number;
45
+ lastRenderWidth: number;
46
+ height: number;
47
+ }
48
+
49
+ export interface ViewerController {
50
+ isOpen(): boolean;
51
+ getFile(): FileNode | null;
52
+ setFile(file: FileNode): void;
53
+ updateFileRef(file: FileNode | null): void;
54
+ close(): void;
55
+ render(width: number): string[];
56
+ handleInput(data: string): ViewerAction;
57
+ }
58
+
59
+ export function createViewer(
60
+ cwd: string,
61
+ theme: Theme,
62
+ requestComment: (payload: CommentPayload, comment: string) => void
63
+ ): ViewerController {
64
+ const state: ViewerState = {
65
+ file: null,
66
+ content: [],
67
+ rawContent: "",
68
+ scroll: 0,
69
+ diffMode: false,
70
+ mode: "normal",
71
+ selectStart: 0,
72
+ selectEnd: 0,
73
+ commentText: "",
74
+ searchQuery: "",
75
+ searchMatches: [],
76
+ searchIndex: 0,
77
+ lastRenderWidth: 0,
78
+ height: DEFAULT_VIEWER_HEIGHT,
79
+ };
80
+
81
+ function resetSearch(): void {
82
+ state.searchQuery = "";
83
+ state.searchMatches = [];
84
+ state.searchIndex = 0;
85
+ }
86
+
87
+ function resetComment(): void {
88
+ state.commentText = "";
89
+ }
90
+
91
+ function clearSelection(): void {
92
+ state.selectStart = 0;
93
+ state.selectEnd = 0;
94
+ }
95
+
96
+ function setMode(mode: ViewerMode): void {
97
+ state.mode = mode;
98
+ if (mode !== "search") resetSearch();
99
+ if (mode !== "comment") resetComment();
100
+ if (mode === "normal") {
101
+ clearSelection();
102
+ }
103
+ }
104
+
105
+ function reloadContent(width: number): void {
106
+ if (!state.file) return;
107
+ const hasChanges = !!state.file.gitStatus;
108
+ state.content = loadFileContent(state.file.path, cwd, state.diffMode, hasChanges, width);
109
+ state.lastRenderWidth = width;
110
+ }
111
+
112
+ function updateSearchMatches(): void {
113
+ state.searchMatches = [];
114
+ if (!state.searchQuery) return;
115
+
116
+ const q = state.searchQuery.toLowerCase();
117
+ const rawLines = state.rawContent.split("\n");
118
+ for (let i = 0; i < rawLines.length; i++) {
119
+ if (rawLines[i].toLowerCase().includes(q)) {
120
+ state.searchMatches.push(i);
121
+ }
122
+ }
123
+ state.searchIndex = 0;
124
+
125
+ if (state.searchMatches.length > 0) {
126
+ state.scroll = Math.max(0, state.searchMatches[0] - SEARCH_SCROLL_OFFSET);
127
+ }
128
+ }
129
+
130
+ function jumpToNextMatch(direction: 1 | -1): void {
131
+ if (state.searchMatches.length === 0) return;
132
+ state.searchIndex += direction;
133
+ if (state.searchIndex < 0) state.searchIndex = state.searchMatches.length - 1;
134
+ if (state.searchIndex >= state.searchMatches.length) state.searchIndex = 0;
135
+ state.scroll = Math.max(0, state.searchMatches[state.searchIndex] - SEARCH_SCROLL_OFFSET);
136
+ }
137
+
138
+ function buildCommentPayload(): CommentPayload | null {
139
+ if (!state.file) return null;
140
+
141
+ const rawLines = state.rawContent.split("\n");
142
+ const selectedText = rawLines.slice(state.selectStart, state.selectEnd + 1).join("\n");
143
+ const relPath = relative(cwd, state.file.path);
144
+ const lineRange = state.selectStart === state.selectEnd
145
+ ? `line ${state.selectStart + 1}`
146
+ : `lines ${state.selectStart + 1}-${state.selectEnd + 1}`;
147
+ const ext = state.file.name.split(".").pop() || "";
148
+
149
+ return { relPath, lineRange, ext, selectedText };
150
+ }
151
+
152
+ function sendComment(comment: string): void {
153
+ const payload = buildCommentPayload();
154
+ if (!payload) return;
155
+
156
+ requestComment(payload, comment);
157
+ setMode("normal");
158
+ }
159
+
160
+ function renderHeader(width: number): string {
161
+ if (!state.file) return "";
162
+ const isUntracked = isUntrackedStatus(state.file.gitStatus);
163
+
164
+ let header = theme.bold(state.file.name);
165
+ if (isUntracked) {
166
+ header += theme.fg("dim", " [UNTRACKED]");
167
+ } else if (state.diffMode) {
168
+ header += theme.fg("warning", " [DIFF]");
169
+ }
170
+ if (state.mode === "select" || state.mode === "comment") {
171
+ header += theme.fg("accent", ` [SELECT ${state.selectStart + 1}-${state.selectEnd + 1}]`);
172
+ }
173
+
174
+ if (state.file.diffStats) {
175
+ if (state.file.diffStats.additions > 0) {
176
+ header += theme.fg("success", ` +${state.file.diffStats.additions}`);
177
+ }
178
+ if (state.file.diffStats.deletions > 0) {
179
+ header += theme.fg("error", ` -${state.file.diffStats.deletions}`);
180
+ }
181
+ } else if (isUntracked && state.file.lineCount !== undefined) {
182
+ header += theme.fg("success", ` +${state.file.lineCount}`);
183
+ }
184
+
185
+ if (state.file.lineCount !== undefined) {
186
+ header += theme.fg("dim", ` ${state.file.lineCount}L`);
187
+ }
188
+
189
+ if (state.mode === "search") {
190
+ header += theme.fg("accent", ` /${state.searchQuery}█`);
191
+ } else if (state.searchQuery && state.searchMatches.length > 0) {
192
+ header += theme.fg("dim", ` [${state.searchIndex + 1}/${state.searchMatches.length}]`);
193
+ }
194
+
195
+ return truncateToWidth(header, width);
196
+ }
197
+
198
+ function renderFooter(width: number): string[] {
199
+ const lines: string[] = [];
200
+ const pct = state.content.length > 0
201
+ ? Math.round((state.scroll / Math.max(1, state.content.length - state.height)) * 100)
202
+ : 0;
203
+
204
+ if (state.mode === "comment") {
205
+ const prompt = theme.fg("accent", `Comment: ${state.commentText}█`);
206
+ lines.push(truncateToWidth(prompt, width));
207
+ lines.push(theme.fg("borderMuted", "─".repeat(width)));
208
+ }
209
+
210
+ let help: string;
211
+ if (state.mode === "comment") {
212
+ help = theme.fg("dim", "Enter: send Esc: cancel");
213
+ } else if (state.mode === "select") {
214
+ help = theme.fg("dim", "j/k: extend c: comment Esc: cancel");
215
+ } else if (state.mode === "search") {
216
+ help = theme.fg("dim", "Type to search Enter: confirm Esc: cancel");
217
+ } else {
218
+ const isUntracked = state.file && isUntrackedStatus(state.file.gitStatus);
219
+ help = theme.fg(
220
+ "dim",
221
+ `j/k: scroll /: search n/N: next/prev match []: files ${state.file?.gitStatus && !isUntracked ? "d: diff " : ""}q: back ${pct}%`
222
+ );
223
+ }
224
+ lines.push(truncateToWidth(help, width));
225
+
226
+ return lines;
227
+ }
228
+
229
+ return {
230
+ isOpen(): boolean {
231
+ return !!state.file;
232
+ },
233
+
234
+ getFile(): FileNode | null {
235
+ return state.file;
236
+ },
237
+
238
+ setFile(file: FileNode): void {
239
+ state.file = file;
240
+ state.scroll = 0;
241
+ state.diffMode = !!file.gitStatus && !isUntrackedStatus(file.gitStatus);
242
+ setMode("normal");
243
+ state.content = [];
244
+ state.lastRenderWidth = 0;
245
+
246
+ try {
247
+ state.rawContent = readFileSync(file.path, "utf-8");
248
+ file.lineCount = state.rawContent.split("\n").length;
249
+ } catch {
250
+ state.rawContent = "";
251
+ file.lineCount = undefined;
252
+ }
253
+ },
254
+
255
+ updateFileRef(file: FileNode | null): void {
256
+ state.file = file;
257
+ },
258
+
259
+ close(): void {
260
+ state.file = null;
261
+ state.content = [];
262
+ setMode("normal");
263
+ },
264
+
265
+ render(width: number): string[] {
266
+ if (!state.file) return [];
267
+
268
+ if (state.lastRenderWidth !== width || state.content.length === 0) {
269
+ reloadContent(width);
270
+ }
271
+
272
+ const lines: string[] = [];
273
+ lines.push(renderHeader(width));
274
+ lines.push(theme.fg("borderMuted", "─".repeat(width)));
275
+
276
+ const visible = state.content.slice(state.scroll, state.scroll + state.height);
277
+ for (let i = 0; i < state.height; i++) {
278
+ if (i < visible.length) {
279
+ const lineIdx = state.scroll + i;
280
+ let line = truncateToWidth(visible[i] || "", width);
281
+ if ((state.mode === "select" || state.mode === "comment") && lineIdx >= state.selectStart && lineIdx <= state.selectEnd) {
282
+ line = theme.bg("selectedBg", line);
283
+ }
284
+ lines.push(line);
285
+ } else {
286
+ lines.push(theme.fg("dim", "~"));
287
+ }
288
+ }
289
+
290
+ lines.push(theme.fg("borderMuted", "─".repeat(width)));
291
+ lines.push(...renderFooter(width));
292
+
293
+ return lines;
294
+ },
295
+
296
+ handleInput(data: string): ViewerAction {
297
+ if (!state.file) return { type: "none" };
298
+
299
+ if (state.mode === "comment") {
300
+ if (matchesKey(data, Key.enter)) {
301
+ const comment = state.commentText.trim();
302
+ if (comment) {
303
+ sendComment(comment);
304
+ } else {
305
+ setMode("normal");
306
+ }
307
+ } else if (matchesKey(data, Key.escape)) {
308
+ setMode("normal");
309
+ } else if (matchesKey(data, Key.backspace)) {
310
+ state.commentText = state.commentText.slice(0, -1);
311
+ } else if (isPrintableChar(data)) {
312
+ state.commentText += data;
313
+ }
314
+ return { type: "none" };
315
+ }
316
+
317
+ if (state.mode === "search") {
318
+ if (matchesKey(data, Key.enter)) {
319
+ setMode("normal");
320
+ } else if (matchesKey(data, Key.escape)) {
321
+ setMode("normal");
322
+ } else if (matchesKey(data, Key.backspace)) {
323
+ state.searchQuery = state.searchQuery.slice(0, -1);
324
+ updateSearchMatches();
325
+ } else if (isPrintableChar(data)) {
326
+ state.searchQuery += data;
327
+ updateSearchMatches();
328
+ }
329
+ return { type: "none" };
330
+ }
331
+
332
+ if (matchesKey(data, "q") && state.mode !== "select") {
333
+ return { type: "close" };
334
+ }
335
+ if (matchesKey(data, Key.escape)) {
336
+ if (state.mode === "select") {
337
+ setMode("normal");
338
+ } else if (state.searchQuery) {
339
+ resetSearch();
340
+ } else {
341
+ return { type: "close" };
342
+ }
343
+ return { type: "none" };
344
+ }
345
+ if (matchesKey(data, "/") && state.mode !== "select") {
346
+ setMode("search");
347
+ return { type: "none" };
348
+ }
349
+ if (matchesKey(data, "n") && state.mode !== "select" && state.searchMatches.length > 0) {
350
+ jumpToNextMatch(1);
351
+ return { type: "none" };
352
+ }
353
+ if (matchesKey(data, "N") && state.mode !== "select" && state.searchMatches.length > 0) {
354
+ jumpToNextMatch(-1);
355
+ return { type: "none" };
356
+ }
357
+ if (matchesKey(data, "j") || matchesKey(data, Key.down)) {
358
+ if (state.mode === "select") {
359
+ state.selectEnd = Math.min(state.content.length - 1, state.selectEnd + 1);
360
+ } else {
361
+ state.scroll = Math.min(Math.max(0, state.content.length - VIEWER_SCROLL_MARGIN), state.scroll + 1);
362
+ }
363
+ return { type: "none" };
364
+ }
365
+ if (matchesKey(data, "k") || matchesKey(data, Key.up)) {
366
+ if (state.mode === "select") {
367
+ state.selectEnd = Math.max(state.selectStart, state.selectEnd - 1);
368
+ } else {
369
+ state.scroll = Math.max(0, state.scroll - 1);
370
+ }
371
+ return { type: "none" };
372
+ }
373
+ if (matchesKey(data, Key.pageDown)) {
374
+ state.scroll = Math.min(Math.max(0, state.content.length - state.height), state.scroll + state.height);
375
+ return { type: "none" };
376
+ }
377
+ if (matchesKey(data, Key.pageUp)) {
378
+ state.scroll = Math.max(0, state.scroll - state.height);
379
+ return { type: "none" };
380
+ }
381
+ if (matchesKey(data, "g")) {
382
+ state.scroll = 0;
383
+ return { type: "none" };
384
+ }
385
+ if (matchesKey(data, "G")) {
386
+ state.scroll = Math.max(0, state.content.length - state.height);
387
+ return { type: "none" };
388
+ }
389
+ if (matchesKey(data, "+") || matchesKey(data, "=")) {
390
+ state.height = Math.min(MAX_VIEWER_HEIGHT, state.height + 5);
391
+ return { type: "none" };
392
+ }
393
+ if (matchesKey(data, "-") || matchesKey(data, "_")) {
394
+ state.height = Math.max(MIN_PANEL_HEIGHT, state.height - 5);
395
+ return { type: "none" };
396
+ }
397
+ if (matchesKey(data, "d") && state.mode !== "select" && state.file.gitStatus && !isUntrackedStatus(state.file.gitStatus)) {
398
+ state.diffMode = !state.diffMode;
399
+ state.lastRenderWidth = 0;
400
+ state.scroll = 0;
401
+ return { type: "none" };
402
+ }
403
+ if (matchesKey(data, "v") && state.mode !== "select") {
404
+ state.mode = "select";
405
+ state.selectStart = state.scroll;
406
+ state.selectEnd = state.scroll;
407
+ return { type: "none" };
408
+ }
409
+ if (matchesKey(data, "c") && state.mode === "select") {
410
+ state.mode = "comment";
411
+ state.commentText = "";
412
+ return { type: "none" };
413
+ }
414
+ if (matchesKey(data, "]") && state.mode !== "select") {
415
+ return { type: "navigate", direction: 1 };
416
+ }
417
+ if (matchesKey(data, "[") && state.mode !== "select") {
418
+ return { type: "navigate", direction: -1 };
419
+ }
420
+
421
+ return { type: "none" };
422
+ },
423
+ };
424
+ }
@@ -0,0 +1,135 @@
1
+ # Importing Claude Code + Codex conversations into Pi — findings
2
+
3
+ ## Pi session format and storage
4
+
5
+ - **Location:** `~/.pi/agent/sessions/--<cwd>--/<timestamp>_<uuid>.jsonl` (cwd is encoded by replacing `/` with `-`).
6
+ - **Format:** JSONL with a header line (`type: "session"`, `version: 3`, `id`, `timestamp`, `cwd`, optional `parentSession`).
7
+ - **Entries:** Each entry is a `SessionEntry` with `id`, `parentId`, `timestamp`. Messages are stored as:
8
+ - `user` message with `content` (string or `[{type:"text"|"image", ...}]`) and `timestamp` (ms).
9
+ - `assistant` message with `api`, `provider`, `model`, `usage`, `stopReason`, `content` blocks (`text`, `thinking`, `toolCall`).
10
+ - `toolResult` messages with `toolCallId`, `toolName`, `content` blocks (`text`/`image`), `isError`.
11
+ - **Tree structure:** Each entry has `parentId`; linear sessions use previous entry ID. `/tree` works off this structure.
12
+ - **Session display name:** `session_info` entries set a display name shown in `/resume`.
13
+
14
+ ## Usage accounting (import shouldn’t count)
15
+
16
+ - `/session` and footer usage totals sum **all assistant message usage** in the session entries (not just post-compaction).
17
+ - Context % in the footer is computed from **the last assistant message’s usage** (input + output + cache read/write).
18
+ - There is **no built‑in “imported” flag** on messages or entries; to exclude imports you’d need to:
19
+ - Set imported assistant `usage` to zero **and** accept that context % will show 0 until a new assistant message arrives, **or**
20
+ - Add a new marker (e.g., `custom` entry or message field) and update footer + stats code to skip imported entries.
21
+
22
+ ## Claude Code transcripts (local)
23
+
24
+ - **User history only:** `~/.claude/history.jsonl` (user commands/prompts; no assistant output).
25
+ - **Full transcripts:** `~/.claude/projects/<project>/<session>.jsonl`.
26
+ - Lines include `type: "user"` and `type: "assistant"` entries with `message` objects plus metadata (`cwd`, `sessionId`, `timestamp`, etc.).
27
+ - Assistant `message.content` can include blocks: `text`, `thinking`, `tool_use` (id, name, input).
28
+ - Tool results show up as **user entries** whose `message.content` contains `tool_result` blocks (with `tool_use_id` and `content`).
29
+ - Other line types include `summary` and `file-history-snapshot` (can be ignored for a v0 importer).
30
+
31
+ ## Codex transcripts (local)
32
+
33
+ - **User history only:** `~/.codex/history.jsonl` (session_id, ts, text; no assistant output).
34
+ - **Full transcripts:** `~/.codex/sessions/YYYY/MM/DD/*.jsonl`.
35
+ - Lines are event records: `{ timestamp, type, payload }`.
36
+ - `type: "response_item"` with `payload.type: "message"` contains `role: "user"|"assistant"` and content blocks (`input_text` / `output_text`).
37
+ - Tool calls appear as `response_item` with `payload.type: "function_call"` (name, arguments, call_id).
38
+ - Tool outputs appear as `response_item` with `payload.type: "function_call_output"` (call_id, output JSON).
39
+ - Many other record types (`event_msg`, `turn_context`, `reasoning`) can be ignored for basic import.
40
+
41
+ ## Mapping notes for a v0 importer
42
+
43
+ - **Create a Pi session JSONL** with a valid header (`version: 3`) and a linear chain of `message` entries.
44
+ - **User messages** map directly from CC `message.role == "user"` and Codex `payload.role == "user"`.
45
+ - **Assistant messages** need Pi’s required fields (`api`, `provider`, `model`, `usage`, `stopReason`, `timestamp`).
46
+ - CC provides model + usage in the assistant `message` payload; Codex logs do **not** provide usage tokens, so you’d likely set them to 0 (unless you add a heuristic).
47
+ - **Tool calls/results:**
48
+ - CC: convert `tool_use` blocks → Pi `toolCall` blocks; convert `tool_result` blocks → Pi `toolResult` messages. Tool name may need to be looked up via the matching `tool_use_id`.
49
+ - Codex: map `function_call` → Pi `toolCall`; `function_call_output` → Pi `toolResult`.
50
+ - **Make imports obvious:**
51
+ - Add a `session_info` entry like `"Imported from Claude Code"` / `"Imported from Codex"` for `/resume` visibility.
52
+ - Optionally add a `custom` entry (non‑LLM) to store source metadata (file path, original session ID, original tool/event IDs).
53
+ - **Resume + share support:** as long as the file is a valid Pi session JSONL in `~/.pi/agent/sessions`, `/resume`, `/tree`, `/export`, and `/share` should work without special casing.
54
+
55
+ ## Open gaps to resolve if we implement
56
+
57
+ - **Imported usage vs. context %**: if imported assistant usage is zero, the footer’s context % will be 0 until a new assistant response is produced.
58
+ - **Missing tool names in CC `tool_result` blocks**: may need to build a `tool_use_id → name` index while parsing.
59
+ - **Model restoration warnings**: if the imported `provider/model` aren’t available locally, Pi will log a restore warning and fall back to the configured default model.
60
+
61
+ ## Spec (draft)
62
+
63
+ ### Goals
64
+ - Import a Claude Code or Codex transcript into a valid Pi session JSONL.
65
+ - Clearly mark imported content as imported in UI and on disk.
66
+ - Exclude imported turns from usage totals; subsequent turns should count normally.
67
+ - V0: after import, allow immediate continuation in Pi (new message appends to the imported session).
68
+ - Later: imported sessions should work with `/resume`, `/tree`, `/export`, `/share` without special cases.
69
+
70
+ ### Non-goals
71
+ - Reconstructing exact token usage for Codex transcripts.
72
+ - Replaying tool execution or restoring filesystem state.
73
+ - Guaranteeing identical model/behavior across agents.
74
+
75
+ ### Inputs
76
+ - Claude Code transcript: `~/.claude/projects/<project>/<session>.jsonl`.
77
+ - Codex transcript: `~/.codex/sessions/YYYY/MM/DD/*.jsonl`.
78
+ - Optional user-only histories (`~/.claude/history.jsonl`, `~/.codex/history.jsonl`) are out of scope for v0.
79
+
80
+ ### Output
81
+ - A Pi session JSONL file in `~/.pi/agent/sessions/--<cwd>--/` with version `3` header.
82
+ - A `session_info` entry naming the source (e.g., "Imported from Claude Code").
83
+ - A `custom` entry with import metadata:
84
+ - `customType: "import"`
85
+ - `data`: `{ source, sourcePath, sourceSessionId, importedAt, originalCwd, originalModel? }`.
86
+
87
+ ### Message mapping
88
+ - **User**: map to Pi `message` entries with `role: "user"` and text/image content.
89
+ - **Assistant**: map to Pi `message` entries with `role: "assistant"` and content blocks.
90
+ - Codex: `usage` defaults to zeros unless we add a heuristic.
91
+ - Claude Code: use `usage`, `provider`, `model` from transcript.
92
+ - **Thinking**: import as `thinking` blocks but hide by default in UI or mark as imported.
93
+ - **Tools**:
94
+ - CC `tool_use` → Pi `toolCall` blocks.
95
+ - CC `tool_result` → Pi `toolResult` messages (resolve `tool_use_id → name`; fallback to `unknown:<short-id>`).
96
+ - Codex `function_call` → Pi `toolCall` blocks.
97
+ - Codex `function_call_output` → Pi `toolResult` messages (store raw output text; optional JSON parse with raw preserved).
98
+ - **Summaries**: CC `summary` → `custom_message` with a visible "Imported summary" label.
99
+ - **Environment context** (Codex): store as `custom` metadata or drop; do not render as user messages.
100
+ - **Aborted assistant messages**: keep only if they contain tool calls or non-empty content; otherwise drop.
101
+ - **Missing images**: replace with a text placeholder like `[Image missing: <path>]`.
102
+
103
+ ### Usage + context accounting
104
+ - Usage totals (`/session`, footer totals) should ignore imported assistant usage.
105
+ - Context % should avoid showing 0% on import; use a deterministic estimate across imported messages until a new assistant response arrives.
106
+ - Imported usage must not be billed or counted against the user’s usage metrics.
107
+ - Track imported range in a `custom` entry (e.g., `{ importedUpToEntryId, importedEntryIds? }`) rather than per-entry schema changes.
108
+
109
+ ### UX expectations
110
+ - **Phase 1 (testing)**: `pi --import <path>` with optional `--source claude|codex` (auto-detect if omitted). No picker in v0; this is for developer testing.
111
+ - **Phase 2**: add `/import` in-session command that mirrors `/resume` UX (cwd/all toggle, lazy loading, same performance profile).
112
+ - **Picker flow** (Phase 2+): selector of recent CC/Codex sessions with preview (first user message + summary text + timestamp), type-to-filter, and optional `--query` CLI flag for direct search using the same fields as `/resume`.
113
+ - **Source labeling**: mirror `/resume` list UI/formatting/columns as baseline; add a clear Claude/Codex indicator (badge/prefix) in the list and in session name.
114
+ - **Post-import**: immediately open the imported session with a status banner (e.g., "Imported from Claude Code — usage excluded").
115
+ - Continuing the chat is seamless: the next user prompt appends to the imported session.
116
+ - `/resume` lists imported sessions alongside normal sessions.
117
+
118
+ ### Error handling
119
+ - Invalid transcript files should fail fast with actionable error messages.
120
+ - Missing model/provider should fall back gracefully; record fallback in import metadata (not as a model change).
121
+ - If tool result mapping fails, import should still proceed with a placeholder tool name.
122
+ - Default to redacting obvious secrets in tool outputs (tokens/keys/Authorization), with an opt-out flag.
123
+
124
+ ### Ordering + timestamps
125
+ - Preserve original timestamps for provenance.
126
+ - If timestamps are identical or go backwards, adjust stored timestamps minimally (e.g., +1ms) while keeping originals in import metadata to preserve UI order.
127
+
128
+ ### Session placement
129
+ - Store the imported session under the current Pi project’s session directory for `/resume` usability.
130
+ - Preserve original `cwd` in import metadata and surface it in the session display name.
131
+
132
+ ### Source scope
133
+ - V0: one source per session (no CC+Codex merge).
134
+ - No read-only mode; `/tree` is available immediately.
135
+ - Idempotent import is optional via a flag (default is always create a new session).
@@ -0,0 +1,79 @@
1
+ # Import Claude Code + Codex Sessions into Pi — Spec
2
+
3
+ ## Overview
4
+ We will add an import pipeline that converts Claude Code and Codex transcripts into Pi session JSONL files. Imported sessions must be clearly labeled, excluded from usage totals, and behave like normal Pi sessions (continuable, resumable, tree‑navigable).
5
+
6
+ ## Goals
7
+ - Import Claude Code or Codex transcripts into valid Pi session JSONL (v3).
8
+ - Mark imported content clearly in UI and on disk.
9
+ - Exclude imported turns from usage totals; subsequent turns count normally.
10
+ - Phase 1: `pi --import <path>` for developer testing.
11
+ - Phase 2: `/import` in-session flow mirroring `/resume` UX.
12
+
13
+ ## Non-goals
14
+ - Perfect token reconstruction for Codex transcripts.
15
+ - Replaying tool execution or restoring filesystem state.
16
+ - Guaranteeing identical model behavior across agents.
17
+
18
+ ## Phases
19
+
20
+ ### Phase 1 (CLI path import)
21
+ - CLI: `pi --import <path>` with optional `--source claude|codex` (auto-detect if omitted).
22
+ - Output session saved under current Pi project sessions directory.
23
+ - Immediately open imported session with a banner (e.g., “Imported from Codex — usage excluded”).
24
+
25
+ ### Phase 2 (interactive import)
26
+ - `/import` command in-session.
27
+ - Picker mirrors `/resume` UI/format/columns, with cwd/all toggle and lazy loading.
28
+ - Search uses the same fields as `/resume`: first user message + summary text + timestamp.
29
+ - Clear source indicator (Claude/Codex badge/prefix) in list and session name.
30
+
31
+ ## Data mapping
32
+
33
+ ### Session header
34
+ - `type: "session"`, `version: 3`, new UUID, `timestamp` from first entry.
35
+ - Preserve original `cwd` in import metadata; store session under current cwd for usability.
36
+
37
+ ### Messages
38
+ - **User**: map to Pi `role: "user"` with text/image content.
39
+ - **Assistant**: map to Pi `role: "assistant"` with text/thinking/toolCall blocks.
40
+ - Claude Code: use provider/model/usage from transcript.
41
+ - Codex: set usage to zero (optional heuristic later).
42
+ - **Thinking**: import, but mark/hide as imported in UI.
43
+ - **Tools**:
44
+ - CC `tool_use` → Pi `toolCall`.
45
+ - CC `tool_result` → Pi `toolResult`, resolve tool name; fallback `unknown:<short-id>`.
46
+ - Codex `function_call` → Pi `toolCall`.
47
+ - Codex `function_call_output` → Pi `toolResult` with raw text; optional JSON parse while preserving raw.
48
+ - **Summaries**: CC `summary` → `custom_message` labeled “Imported summary”.
49
+ - **Environment context** (Codex): store in `custom` metadata or drop; never show as a user message.
50
+ - **Aborted assistant messages**: keep only if non-empty or include tool calls.
51
+ - **Missing images**: replace with `[Image missing: <path>]` text block.
52
+
53
+ ### Import metadata
54
+ - Append a `custom` entry: `customType: "import"` with `{ source, sourcePath, sourceSessionId, importedAt, originalCwd, originalModel?, originalTimestamps? }`.
55
+ - Track imported range in metadata (e.g., `importedUpToEntryId` or `importedEntryIds`).
56
+
57
+ ## Usage + context accounting
58
+ - Imported assistant usage does **not** count toward totals.
59
+ - Context % uses deterministic estimate over imported messages until a new assistant response arrives.
60
+ - Imported usage never billed.
61
+
62
+ ## Ordering + timestamps
63
+ - Preserve original timestamps.
64
+ - If timestamps collide/go backward, adjust minimally (e.g., +1ms) to preserve UI order while storing originals in metadata.
65
+
66
+ ## UX + labeling
67
+ - Imported sessions show a clear source indicator in list and session name.
68
+ - `/resume` lists imported sessions alongside normal sessions.
69
+ - No read‑only state: `/tree` works immediately.
70
+
71
+ ## Error handling
72
+ - Fail fast with actionable errors for invalid transcript files.
73
+ - Missing model/provider falls back gracefully; record fallback in import metadata.
74
+ - Tool mapping failures proceed with placeholder tool name.
75
+ - Redact obvious secrets in tool outputs by default (tokens/keys/Authorization), with an opt‑out flag.
76
+
77
+ ## Scope constraints
78
+ - One source per session (no CC+Codex merge) in v0.
79
+ - Idempotent imports optional via flag; default is always create new session.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "pi-extensions",
3
+ "version": "0.1.9",
4
+ "private": false,
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
8
+ "pi": {
9
+ "extensions": [
10
+ "./agent-guidance/agent-guidance.ts",
11
+ "./arcade/spice-invaders.ts",
12
+ "./arcade/picman.ts",
13
+ "./arcade/ping.ts",
14
+ "./arcade/tetris.ts",
15
+ "./arcade/mario-not/mario-not.ts",
16
+ "./code-actions/index.ts",
17
+ "./files-widget/index.ts",
18
+ "./raw-paste/index.ts",
19
+ "./ralph-wiggum/index.ts",
20
+ "./relaunch/index.ts",
21
+ "./tab-status/tab-status.ts",
22
+ "./usage-extension/index.ts",
23
+ "./ready-status/ready-status.ts"
24
+ ],
25
+ "skills": [
26
+ "./ralph-wiggum/SKILL.md"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-01-25
4
+ - Clarified that agents must write the task file themselves (tool does not auto-create it).
5
+
6
+ ## 0.1.0 - 2026-01-13
7
+ - Initial release.