Skip to content

Commit cd87682

Browse files
authored
refactor: improve type safety and ui consistency across extensions (#13)
* refactor: improve type safety and ui consistency across extensions * refactor: improve code clarity and move type checking to lint script * docs: update changelog
1 parent ae2b3d4 commit cd87682

17 files changed

Lines changed: 363 additions & 2129 deletions

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ All notable changes to agent-stuff are documented here.
2222

2323

2424

25-
## feat/force-push-on-rejection
25+
26+
27+
## refactor/type-safety-and-ui-updates
28+
29+
Improved type safety across the extension system by adding explicit TypeScript type annotations and migrating from string-based to typed enum patterns for UI colors and notification levels (#13). Reorganized the linting pipeline to include TypeScript type checking via `tsc --noEmit`, catching previously undetected type errors in message content handling, component rendering, and event data access. Standardized UI component property names for consistency (e.g., `selectedBg``selectedPrefix`, `matchHighlight``selectedText`) and updated notification severity levels to use semantic theme colors (`success`/`info``info`/`accent`). Removed ~2,100 lines of unused code and added comprehensive test coverage for the plan-ask extension, improving maintainability and reducing technical debt.
30+
31+
## [1.0.1](https://github.com/kostyay/agent-stuff/pull/12) - 2026-03-02
2632

2733
Enhanced the push workflow to gracefully handle rejected pushes by prompting users to force-push with `--force-with-lease` (#12). When a normal push fails due to non-fast-forward errors (common after rebasing), users are now prompted to safely force-push rather than failing silently. The implementation includes rejection detection via stderr pattern matching and proper error handling for both standard and force-push attempts, with user-facing notifications at each step.
2834

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"readme": "README.md",
1010
"type": "module",
1111
"scripts": {
12-
"lint": "eslint --fix pi-extensions/ tests/",
12+
"lint": "eslint --fix pi-extensions/ tests/ && npx tsc --noEmit",
1313
"test": "npm run lint && node --experimental-strip-types --test tests/*.test.ts"
1414
},
1515
"keywords": [

pi-extensions/answer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,11 @@ class QnAComponent implements Component {
163163
const editorTheme: EditorTheme = {
164164
borderColor: this.dim,
165165
selectList: {
166-
selectedBg: (s: string) => `\x1b[44m${s}\x1b[0m`,
167-
matchHighlight: this.cyan,
168-
itemSecondary: this.gray,
166+
selectedPrefix: (s: string) => `\x1b[44m${s}\x1b[0m`,
167+
selectedText: this.cyan,
168+
description: this.gray,
169+
scrollInfo: this.dim,
170+
noMatch: this.dim,
169171
},
170172
};
171173

pi-extensions/control.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ function getMessagesSinceLastPrompt(ctx: ExtensionContext): ExtractedMessage[] {
472472
if (entry.type === "message") {
473473
const msg = entry.message;
474474
if ("role" in msg && (msg.role === "user" || msg.role === "assistant")) {
475-
const textParts = msg.content
475+
const content = msg.content;
476+
const textParts = (Array.isArray(content) ? content : [])
476477
.filter((c): c is { type: "text"; text: string } => c.type === "text")
477478
.map((c) => c.text);
478479
if (textParts.length > 0) {
@@ -764,7 +765,8 @@ async function handleCommand(
764765
return;
765766
}
766767

767-
respond(false, command.type, undefined, `Unsupported command: ${command.type}`);
768+
const commandType = (command as { type: string }).type;
769+
respond(false, commandType, undefined, `Unsupported command: ${commandType}`);
768770
}
769771

770772
// ============================================================================
@@ -868,12 +870,12 @@ async function sendRpcCommand(
868870
// Handle response
869871
if (msg.type === "response") {
870872
if (msg.command === command.type) {
871-
response = msg;
873+
response = msg as RpcResponse;
872874
// If not waiting for event, we're done
873875
if (!waitForEvent) {
874876
cleanup();
875877
socket.end();
876-
resolve({ response });
878+
resolve({ response: msg as RpcResponse });
877879
return;
878880
}
879881
}
@@ -1403,11 +1405,12 @@ Messages automatically include sender session info for replies. When you want a
14031405

14041406
renderResult(result, { expanded }, theme) {
14051407
const details = result.details as Record<string, unknown> | undefined;
1406-
const isError = result.isError === true;
1408+
const isError = (result as unknown as Record<string, unknown>).isError === true;
14071409

14081410
// Error case
14091411
if (isError || details?.error) {
1410-
const errorMsg = (details?.error as string) || result.content[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "Unknown error";
1412+
const errorMsg = (details?.error as string)
1413+
?? (result.content[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "Unknown error");
14111414
return new Text(theme.fg("error", "✗ ") + theme.fg("error", errorMsg), 0, 0);
14121415
}
14131416

pi-extensions/files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ const extractFileReferencesFromContent = (content: unknown): string[] => {
159159
};
160160

161161
const extractFileReferencesFromEntry = (entry: SessionEntry): string[] => {
162-
if (entry.type === "message") {
162+
if (entry.type === "message" && "content" in entry.message) {
163163
return extractFileReferencesFromContent(entry.message.content);
164164
}
165165

pi-extensions/git-rebase-master.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export default function gitRebaseMasterExtension(pi: ExtensionAPI) {
154154
]);
155155

156156
if (rebaseCode === 0) {
157-
ctx.ui.notify("Rebase completed successfully — no conflicts.", "success");
157+
ctx.ui.notify("Rebase completed successfully — no conflicts.", "info");
158158
return;
159159
}
160160

pi-extensions/kt/index.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type ExtensionAPI,
2525
type ExtensionContext,
2626
type Theme,
27+
type ThemeColor,
2728
} from "@mariozechner/pi-coding-agent";
2829
import { StringEnum } from "@mariozechner/pi-ai";
2930
import { Type } from "@sinclair/typebox";
@@ -165,8 +166,8 @@ async function withLock<T>(
165166
// ── Themed Rendering ───────────────────────────────────────────────────
166167

167168
function renderTicketHeading(theme: Theme, t: TicketFrontMatter, currentSession?: string): string {
168-
let iconColor: string;
169-
let titleColor: string;
169+
let iconColor: ThemeColor;
170+
let titleColor: ThemeColor;
170171
if (t.status === "closed") {
171172
iconColor = "dim";
172173
titleColor = "dim";
@@ -1140,7 +1141,7 @@ export default function ktExtension(pi: ExtensionAPI) {
11401141

11411142
if (nextPrompt) {
11421143
ctx.ui.setEditorText(nextPrompt);
1143-
rootTui?.requestRender();
1144+
(rootTui as TUI | null)?.requestRender();
11441145
}
11451146
},
11461147
});
@@ -1200,18 +1201,16 @@ export default function ktExtension(pi: ExtensionAPI) {
12001201
}
12011202

12021203
// Ask user how to proceed
1203-
const forkChoice = await ctx.ui.select(
1204-
"kt-run-all: Session strategy",
1205-
[
1206-
{ value: "fork-each", label: "Fork a new session for each ticket", description: "Isolated sessions, context injected between tickets" },
1207-
{ value: "same-session", label: "Work through all in this session", description: "Agent processes tickets sequentially in current session" },
1208-
{ value: "cancel", label: "Cancel" },
1209-
],
1210-
);
1211-
1212-
if (!forkChoice || forkChoice === "cancel") return;
1213-
1214-
if (forkChoice === "same-session") {
1204+
const forkOptions = [
1205+
"Fork a new session for each ticket",
1206+
"Work through all in this session",
1207+
"Cancel",
1208+
];
1209+
const forkChoice = await ctx.ui.select("kt-run-all: Session strategy", forkOptions);
1210+
1211+
if (!forkChoice || forkChoice === "Cancel") return;
1212+
1213+
if (forkChoice === "Work through all in this session") {
12151214
// Inject prompt to work through all tickets
12161215
const prompt =
12171216
`Work through these tickets in order, one at a time. For each ticket:\n` +

pi-extensions/loop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
219219

220220
pi.sendMessage({
221221
customType: "loop",
222-
content: loopState.prompt,
222+
content: loopState.prompt ?? "",
223223
display: true
224224
}, {
225225
deliverAs: "followUp",

pi-extensions/notify.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,15 @@ const formatNotification = (text: string | null): { title: string; body: string
7979
return { title: "π", body };
8080
};
8181

82-
export default function (pi: ExtensionAPI) {
82+
export default function notifyExtension(pi: ExtensionAPI) {
8383
pi.on("agent_end", async (event) => {
8484
const lastText = extractLastAssistantText(event.messages ?? []);
8585
const { title, body } = formatNotification(lastText);
8686
notify(title, body);
8787
});
8888

89-
pi.events.on("waiting_for_input", (data: { question?: string }) => {
90-
notify("π", data.question ?? "Waiting for input");
89+
pi.events.on("waiting_for_input", (data: unknown) => {
90+
const input = data as { question?: string } | undefined;
91+
notify("π", input?.question ?? "Waiting for input");
9192
});
9293
}

pi-extensions/plan-ask.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import type { AgentMessage } from "@mariozechner/pi-agent-core";
2323
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
24-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
24+
import type { ExtensionAPI, ExtensionContext, ThemeColor } from "@mariozechner/pi-coding-agent";
2525

2626
// ── Types ────────────────────────────────────────────────────────────────
2727

@@ -242,15 +242,17 @@ const DEFAULT_AGENT_TOOLS: string[] = ["read", "bash", "edit", "write"];
242242

243243
// ── Status display ───────────────────────────────────────────────────────
244244

245-
interface ModeDisplay {
245+
/** Display metadata for each mode — icon, label, and theme color. */
246+
export interface ModeDisplay {
246247
icon: string;
247248
label: string;
248-
color: "success" | "info" | "warning";
249+
color: ThemeColor;
249250
}
250251

251-
const MODE_DISPLAY: Record<Mode, ModeDisplay> = {
252+
/** Per-mode display configuration used in the status bar and notifications. */
253+
export const MODE_DISPLAY: Record<Mode, ModeDisplay> = {
252254
[AGENT]: { icon: "🤖", label: "agent", color: "success" },
253-
[ASK]: { icon: "❓", label: "ask", color: "info" },
255+
[ASK]: { icon: "❓", label: "ask", color: "accent" },
254256
[PLAN]: { icon: "📋", label: "plan", color: "warning" },
255257
};
256258

@@ -431,7 +433,7 @@ export default function planAskExtension(pi: ExtensionAPI) {
431433

432434
if (action === "Create tickets with /kt-create") {
433435
setMode(AGENT, ctx);
434-
ctx.ui.runCommand("kt-create");
436+
pi.sendUserMessage("/kt-create");
435437
return;
436438
}
437439

@@ -454,7 +456,7 @@ export default function planAskExtension(pi: ExtensionAPI) {
454456
const { dirname } = await import("node:path");
455457
await mkdir(dirname(path), { recursive: true });
456458
await writeFile(path, planText, "utf-8");
457-
ctx.ui.notify(`Plan saved to ${path}`, "success");
459+
ctx.ui.notify(`Plan saved to ${path}`, "info");
458460
} catch (err: unknown) {
459461
const message = err instanceof Error ? err.message : String(err);
460462
ctx.ui.notify(`Failed to save plan: ${message}`, "error");

0 commit comments

Comments
 (0)