@samy-clivolt/create-pi-ag-ui 0.1.2

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 (39) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +236 -0
  3. package/package.json +40 -0
  4. package/template/README.md +408 -0
  5. package/template/_env.example +27 -0
  6. package/template/_eslintrc.json +3 -0
  7. package/template/_gitignore +37 -0
  8. package/template/justfile +19 -0
  9. package/template/next-env.d.ts +6 -0
  10. package/template/next.config.ts +13 -0
  11. package/template/package.json +42 -0
  12. package/template/postcss.config.js +6 -0
  13. package/template/src/app/api/copilotkit/route.ts +63 -0
  14. package/template/src/app/api/models/route.ts +77 -0
  15. package/template/src/app/api/thinking/route.ts +38 -0
  16. package/template/src/app/globals.css +130 -0
  17. package/template/src/app/layout.tsx +19 -0
  18. package/template/src/app/page.tsx +214 -0
  19. package/template/src/components/ActivityPanel.tsx +83 -0
  20. package/template/src/components/AgentExtrasPanel.tsx +21 -0
  21. package/template/src/components/AgentStatePanel.tsx +194 -0
  22. package/template/src/components/ChatToolbar.tsx +42 -0
  23. package/template/src/components/ChatUI.tsx +258 -0
  24. package/template/src/components/FrontendTools.tsx +509 -0
  25. package/template/src/components/InterruptHandler.tsx +183 -0
  26. package/template/src/components/ModelPicker.tsx +179 -0
  27. package/template/src/components/PiAgentProvider.tsx +28 -0
  28. package/template/src/components/ThinkingBlock.tsx +70 -0
  29. package/template/src/components/ThinkingPicker.tsx +112 -0
  30. package/template/src/components/ThreadSwitcher.tsx +51 -0
  31. package/template/src/components/ToolRenderers.tsx +409 -0
  32. package/template/src/components/chat/MessageSkeleton.tsx +17 -0
  33. package/template/src/components/chat/StreamingIndicator.tsx +37 -0
  34. package/template/src/components/layout/MobileSidebarDrawer.tsx +82 -0
  35. package/template/src/components/state/MetricsCard.tsx +36 -0
  36. package/template/src/components/state/StepTimeline.tsx +56 -0
  37. package/template/src/lib/agent-state-context.tsx +183 -0
  38. package/template/tailwind.config.ts +11 -0
  39. package/template/tsconfig.json +40 -0
@@ -0,0 +1,183 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AgentStateContext
5
+ *
6
+ * Centralises all useCoAgentStateRender({ name: "pi-agent" }) calls into ONE hook.
7
+ * CopilotKit only honours one handler per agent name — all components must read
8
+ * from this shared context instead of registering their own hooks.
9
+ *
10
+ * Usage:
11
+ * const state = useAgentState(); // full AgentSharedState (or null)
12
+ * const isStreaming = useIsStreaming(); // convenience bool
13
+ */
14
+
15
+ import { useCoAgentStateRender } from "@copilotkit/react-core";
16
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
17
+ import {
18
+ EMPTY_ACTIVITY_SUMMARY,
19
+ EMPTY_RUN_METRICS,
20
+ type ActivityEntry,
21
+ type ActivitySummary,
22
+ type AgentSharedState,
23
+ type RunMetrics,
24
+ type StepEntry,
25
+ } from "@samy-clivolt/pi-ag-ui/types";
26
+
27
+ // ─── Normalisation ────────────────────────────────────────────────────
28
+
29
+ function normalizeActivities(value: unknown): ActivityEntry[] {
30
+ if (!Array.isArray(value)) return [];
31
+ return value
32
+ .map((item) => {
33
+ if (!item || typeof item !== "object") return null;
34
+ const a = item as Record<string, unknown>;
35
+ if (typeof a.id !== "string" || typeof a.tool !== "string") return null;
36
+ return {
37
+ id: a.id,
38
+ tool: a.tool,
39
+ status: typeof a.status === "string" ? a.status : "unknown",
40
+ startedAt: typeof a.startedAt === "number" ? a.startedAt : Date.now(),
41
+ endedAt: typeof a.endedAt === "number" ? a.endedAt : null,
42
+ durationMs: typeof a.durationMs === "number" ? a.durationMs : null,
43
+ error: typeof a.error === "string" ? a.error : null,
44
+ };
45
+ })
46
+ .filter((x): x is ActivityEntry => x !== null);
47
+ }
48
+
49
+ function normalizeSteps(value: unknown): StepEntry[] {
50
+ if (!Array.isArray(value)) return [];
51
+ return value
52
+ .map((item) => {
53
+ if (!item || typeof item !== "object") return null;
54
+ const s = item as Record<string, unknown>;
55
+ if (typeof s.id !== "string") return null;
56
+ return {
57
+ id: s.id,
58
+ label: typeof s.label === "string" ? s.label : s.id,
59
+ status: (s.status === "complete" || s.status === "error") ? s.status : "running" as const,
60
+ startedAt: typeof s.startedAt === "number" ? s.startedAt : Date.now(),
61
+ endedAt: typeof s.endedAt === "number" ? s.endedAt : null,
62
+ durationMs: typeof s.durationMs === "number" ? s.durationMs : null,
63
+ error: typeof s.error === "string" ? s.error : null,
64
+ };
65
+ })
66
+ .filter((x): x is StepEntry => x !== null);
67
+ }
68
+
69
+ function normalizeActivitySummary(value: unknown, activities: ActivityEntry[]): ActivitySummary {
70
+ if (value && typeof value === "object") {
71
+ const s = value as Record<string, unknown>;
72
+ if (typeof s.runningCount === "number" && typeof s.recentCount === "number"
73
+ && typeof s.errorCount === "number" && typeof s.total === "number") {
74
+ return { runningCount: s.runningCount, recentCount: s.recentCount, errorCount: s.errorCount, total: s.total };
75
+ }
76
+ }
77
+ const runningCount = activities.filter((a) => isRunningStatus(a.status)).length;
78
+ const recentCount = activities.length - runningCount;
79
+ return { runningCount, recentCount, errorCount: activities.filter((a) => a.status === "error").length, total: activities.length };
80
+ }
81
+
82
+ function normalizeRunMetrics(value: unknown): RunMetrics {
83
+ if (!value || typeof value !== "object") return { ...EMPTY_RUN_METRICS };
84
+ const m = value as Record<string, unknown>;
85
+ return {
86
+ estimatedInputTokens: typeof m.estimatedInputTokens === "number" ? m.estimatedInputTokens : 0,
87
+ estimatedOutputTokens: typeof m.estimatedOutputTokens === "number" ? m.estimatedOutputTokens : 0,
88
+ estimatedTotalTokens: typeof m.estimatedTotalTokens === "number" ? m.estimatedTotalTokens : 0,
89
+ estimatedCostUsd: typeof m.estimatedCostUsd === "number" ? m.estimatedCostUsd : 0,
90
+ estimationMode: (m.estimationMode === "usage" || m.estimationMode === "hybrid") ? m.estimationMode : "heuristic",
91
+ updatedAt: typeof m.updatedAt === "number" ? m.updatedAt : 0,
92
+ };
93
+ }
94
+
95
+ export function isRunningStatus(status: string): boolean {
96
+ return status === "running" || status === "executing" || status === "inProgress";
97
+ }
98
+
99
+ export function normalizeAgentState(input: Record<string, unknown>): AgentSharedState {
100
+ const activities = normalizeActivities(input.activities);
101
+ const stepTimeline = normalizeSteps(input.stepTimeline);
102
+ const activitySummary = normalizeActivitySummary(input.activitySummary, activities);
103
+ const runMetrics = normalizeRunMetrics(input.runMetrics);
104
+
105
+ return {
106
+ isStreaming: Boolean(input.isStreaming),
107
+ model: typeof input.model === "string" ? input.model : undefined,
108
+ thinkingLevel: typeof input.thinkingLevel === "string" ? input.thinkingLevel : "off",
109
+ piMessageCount: typeof input.piMessageCount === "number" ? input.piMessageCount : 0,
110
+ currentStep: typeof input.currentStep === "string" ? input.currentStep : null,
111
+ isThinking: Boolean(input.isThinking),
112
+ activities,
113
+ stepTimeline,
114
+ activitySummary,
115
+ runMetrics,
116
+ estimatedTokens: typeof input.estimatedTokens === "number" ? input.estimatedTokens : runMetrics.estimatedTotalTokens,
117
+ estimatedCostUsd: typeof input.estimatedCostUsd === "number" ? input.estimatedCostUsd : runMetrics.estimatedCostUsd,
118
+ codingToolsEnabled: Boolean(input.codingToolsEnabled),
119
+ cwd: typeof input.cwd === "string" ? input.cwd : null,
120
+ threadId: typeof input.threadId === "string" ? input.threadId : "default",
121
+ activeSessionCount: typeof input.activeSessionCount === "number" ? input.activeSessionCount : 0,
122
+ persistenceEnabled: Boolean(input.persistenceEnabled),
123
+ sessionId: typeof input.sessionId === "string" ? input.sessionId : null,
124
+ extensionCount: typeof input.extensionCount === "number" ? input.extensionCount : 0,
125
+ custom: typeof input.custom === "object" && input.custom ? (input.custom as Record<string, unknown>) : {},
126
+ };
127
+ }
128
+
129
+ // ─── Context ──────────────────────────────────────────────────────────
130
+
131
+ const AgentStateContext = createContext<AgentSharedState | null>(null);
132
+
133
+ export function useAgentState(): AgentSharedState | null {
134
+ return useContext(AgentStateContext);
135
+ }
136
+
137
+ // ─── Inner syncer (returned from render callback) ─────────────────────
138
+
139
+ function AgentStateSyncer({
140
+ raw,
141
+ onSync,
142
+ }: {
143
+ raw: Record<string, unknown>;
144
+ onSync: (s: AgentSharedState) => void;
145
+ }) {
146
+ useEffect(() => {
147
+ onSync(normalizeAgentState(raw));
148
+ });
149
+ return null;
150
+ }
151
+
152
+ // ─── Provider ─────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Wrap children with this provider to give them access to the shared Pi agent state.
156
+ * Must be inside <CopilotKit>.
157
+ */
158
+ export function AgentStateProvider({ children }: { children: React.ReactNode }) {
159
+ const [agentState, setAgentState] = useState<AgentSharedState | null>(null);
160
+ const setRef = useRef(setAgentState);
161
+ setRef.current = setAgentState;
162
+
163
+ useCoAgentStateRender({
164
+ name: "pi-agent",
165
+ render: ({ state }: { state: unknown }) => {
166
+ if (state && typeof state === "object") {
167
+ return (
168
+ <AgentStateSyncer
169
+ raw={state as Record<string, unknown>}
170
+ onSync={(s) => setRef.current(s)}
171
+ />
172
+ );
173
+ }
174
+ return null;
175
+ },
176
+ });
177
+
178
+ return (
179
+ <AgentStateContext.Provider value={agentState}>
180
+ {children}
181
+ </AgentStateContext.Provider>
182
+ );
183
+ }
@@ -0,0 +1,11 @@
1
+ import type { Config } from "tailwindcss";
2
+
3
+ const config: Config = {
4
+ content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
5
+ theme: {
6
+ extend: {},
7
+ },
8
+ plugins: [],
9
+ };
10
+
11
+ export default config;
@@ -0,0 +1,40 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "preserve",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./src/*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "**/*.ts",
33
+ "**/*.tsx",
34
+ "next-env.d.ts",
35
+ ".next/types/**/*.ts"
36
+ ],
37
+ "exclude": [
38
+ "node_modules"
39
+ ]
40
+ }